Source

Strelka / lib / strelka / paramvalidator.rb

The default branch has multiple heads

Full commit
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
# -*- ruby -*-
# vim: set nosta noet ts=4 sw=4:
# encoding: utf-8

#encoding: utf-8

require 'uri'
require 'forwardable'
require 'date'
require 'formvalidator'
require 'loggability'

require 'strelka/mixins'
require 'strelka' unless defined?( Strelka )
require 'strelka/app' unless defined?( Strelka::App )


# A validator for user parameters.
#
# == Usage
#
#	require 'strelka/paramvalidator'
#
#	validator = Strelka::ParamValidator.new
#
#	# Add validation criteria for input parameters
#	validator.add( :name, /^(?<lastname>\S+), (?<firstname>\S+)$/, "Customer Name" )
#	validator.add( :email, "Customer Email" )
#	validator.add( :feedback, :printable, "Customer Feedback" )
#	validator.override( :email, :printable, "Your Email Address" )
#
#	# Untaint all parameter values which match their constraints
#	validate.untaint_all_constraints = true
#
#	# Now pass in tainted values in a hash (e.g., from an HTML form)
#	validator.validate( req.params )
#
#	# Now if there weren't any errors, use some form values to fill out the
#	# success page template
#	if validator.okay?
#		tmpl = template :success
#		tmpl.firstname = validator[:name][:firstname]
#		tmpl.lastname  = validator[:name][:lastname]
#		tmpl.email	   = validator[:email]
#		tmpl.feedback  = validator[:feedback]
#		return tmpl
#
#	# Otherwise fill in the error template with auto-generated error messages
#	# and return that instead.
#	else
#		tmpl = template :feedback_form
#		tmpl.errors = validator.error_messages
#		return tmpl
#	end
#
class Strelka::ParamValidator < ::FormValidator
	extend Forwardable,
		   Loggability,
		   Strelka::MethodUtilities
	include Strelka::DataUtilities

	# Loggability API -- log to the 'strelka' logger
	log_to :strelka


	# Pattern for countint the number of hash levels in a parameter key
	PARAMS_HASH_RE = /^([^\[]+)(\[.*\])?(.)?.*$/

	# Pattern to use to strip binding operators from parameter patterns so they
	# can be used in the middle of routing Regexps.
	PARAMETER_PATTERN_STRIP_RE = Regexp.union( '^', '$', '\\A', '\\z', '\\Z' )



	# The base constraint type.
	class Constraint
		extend Loggability,
		       Strelka::MethodUtilities

		# Loggability API -- log to the 'strelka' logger
		log_to :strelka


		# Flags that are passed as Symbols when declaring a parameter
		FLAGS = [ :required, :untaint, :multiple ]

		# Map of constraint specification types to their equivalent Constraint class.
		TYPES = { Proc => self }


		### Register the given +subclass+ as the Constraint class to be used when
		### the specified +syntax_class+ is given as the constraint in a parameter
		### declaration.
		def self::register_type( syntax_class )
			self.log.debug "Registering %p as the constraint class for %p objects" %
				[ self, syntax_class ]
			TYPES[ syntax_class ] = self
		end


		### Return a Constraint object appropriate for the given +field+ and +spec+.
		def self::for( field, spec=nil, *options, &block )
			self.log.debug "Building Constraint for %p (%p)" % [ field, spec ]

			# Handle omitted constraint
			if spec.is_a?( String ) || FLAGS.include?( spec )
				options.unshift( spec )
				spec = nil
			end

			spec ||= block

			subtype = TYPES[ spec.class ] or
				raise "No constraint type for a %p validation spec" % [ spec.class ]

			return subtype.new( field, spec, *options, &block )
		end


		### Create a new Constraint for the field with the given +name+, configuring it with the
		### specified +args+. The +block+ is what does the actual validation, at least in the
		### base class.
		def initialize( name, *args, &block )
			@name		 = name
			@block		 = block

			@description = args.shift if args.first.is_a?( String )

			@required	 = args.include?( :required )
			@untaint	 = args.include?( :untaint )
			@multiple	 = args.include?( :multiple )
		end


		######
		public
		######

		# The name of the field the constraint governs
		attr_reader :name

		# The constraint's check block
		attr_reader :block

		# The field's description
		attr_writer :description

		##
		# Returns true if the field can have multiple values.
		attr_predicate :multiple?

		##
		# Returns true if the field associated with the constraint is required in
		# order for the parameters to be valid.
		attr_predicate :required?

		##
		# Returns true if the constraint will also untaint its result before returning it.
		attr_predicate :untaint?


		### Check the given value against the constraint and return the result if it passes.
		def apply( value, force_untaint=false )
			untaint = self.untaint? || force_untaint

			if self.multiple?
				return self.check_multiple( value, untaint )
			else
				return self.check( value, untaint )
			end
		end


		### Comparison operator – Constraints are equal if they’re for the same field,
		### they’re of the same type, and their blocks are the same.
		def ==( other )
			return self.name == other.name &&
				other.instance_of?( self.class ) &&
				self.block == other.block
		end


		### Get the description of the field.
		def description
			return @description || self.generate_description
		end


		### Return the constraint expressed as a String.
		def to_s
			desc = self.validator_description

			flags = []
			flags << 'required' if self.required?
			flags << 'multiple' if self.multiple?
			flags << 'untaint' if self.untaint?

			desc << " (%s)" % [ flags.join(',') ] unless flags.empty?

			return desc
		end


		#########
		protected
		#########

		### Return a description of the validation provided by the constraint object.
		def validator_description
			desc = 'a custom validator'

			if self.block
				location = self.block.source_location
				desc << " on line %d of %s" % [ location[1], location[0] ]
			end

			return desc
		end


		### Check the specified value against the constraint and return the results. By
		### default, this just calls to_proc and the block and calls the result with the
		### value as its argument.
		def check( value, untaint )
			return self.block.to_proc.call( value ) if self.block
			value.untaint if untaint && value.respond_to?( :untaint )
			return value
		end


		### Check the given +values+ against the constraint and return the results if
		### all of them succeed.
		def check_multiple( values, untaint )
			values = [ values ] unless values.is_a?( Array )
			results = []

			values.each do |value|
				result = self.check( value, untaint ) or return nil
				results << result
			end

			return results
		end


		### Generate a description from the name of the field.
		def generate_description
			self.log.debug "Auto-generating description for %p" % [ self ]
			desc = self.name.to_s.
				gsub( /.*\[(\w+)\]/, "\\1" ).
				gsub( /_(.)/ ) {|m| " " + m[1,1].upcase }.
				gsub( /^(.)/ ) {|m| m.upcase }
			self.log.debug "  generated: %p" % [ desc ]
			return desc
		end

	end # class Constraint


	# A constraint expressed as a regular expression.
	class RegexpConstraint < Constraint

		# Use this for constraints expressed as Regular Expressions
		register_type Regexp


		### Create a new RegexpConstraint that will validate the field of the given
		### +name+ with the specified +pattern+.
		def initialize( name, pattern, *args, &block )
			@pattern = pattern

			super( name, *args, &block )
		end


		######
		public
		######

		# The constraint's pattern
		attr_reader :pattern


		### Check the +value+ against the regular expression and return its
		### match groups if successful.
		def check( value, untaint )
			self.log.debug "Validating %p via regexp %p" % [ value, self.pattern ]
			match = self.pattern.match( value.to_s ) or return nil

			if match.captures.empty?
				self.log.debug "  no captures, using whole match: %p" % [match[0]]
				return super( match[0], untaint )

			elsif match.names.length > 1
				self.log.debug "  extracting hash of named captures: %p" % [ match.names ]
				rhash = self.matched_hash( match, untaint )
				return super( rhash, untaint )

			elsif match.captures.length == 1
				self.log.debug "  extracting one capture: %p" % [match.captures.first]
				return super( match.captures.first, untaint )

			else
				self.log.debug "  extracting multiple captures: %p" % [match.captures]
				values = match.captures
				values.map {|val| val.untaint if val } if untaint
				return super( values, untaint )
			end
		end


		### Return a Hash of the given +match+ object's named captures, untainting the values
		### if +untaint+ is true.
		def matched_hash( match, untaint )
			return match.names.inject( {} ) do |accum,name|
				value = match[ name ]
				value.untaint if untaint && value
				accum[ name.to_sym ] = value
				accum
			end
		end


		### Return the constraint expressed as a String.
		def validator_description
			return "a value matching the pattern %p" % [ self.pattern ]
		end


	end # class RegexpConstraint


	# A constraint class that uses a collection of predefined patterns.
	class BuiltinConstraint < RegexpConstraint

		# Use this for constraints expressed as Symbols or who are missing a constraint spec (nil)
		register_type Symbol
		register_type NilClass


		#
		# RFC822 Email Address Regex
		# --------------------------
		#
		# Originally written by Cal Henderson
		# c.f. http://iamcal.com/publish/articles/php/parsing_email/
		#
		# Translated to Ruby by Tim Fletcher, with changes suggested by Dan Kubb.
		#
		# Licensed under a Creative Commons Attribution-ShareAlike 2.5 License
		# http://creativecommons.org/licenses/by-sa/2.5/
		#
		RFC822_EMAIL_ADDRESS = begin
			qtext = '[^\\x0d\\x22\\x5c\\x80-\\xff]'
			dtext = '[^\\x0d\\x5b-\\x5d\\x80-\\xff]'
			atom = '[^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-' +
				'\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+'
			quoted_pair = '\\x5c[\\x00-\\x7f]'
			domain_literal = "\\x5b(?:#{dtext}|#{quoted_pair})*\\x5d"
			quoted_string = "\\x22(?:#{qtext}|#{quoted_pair})*\\x22"
			domain_ref = atom
			sub_domain = "(?:#{domain_ref}|#{domain_literal})"
			word = "(?:#{atom}|#{quoted_string})"
			domain = "#{sub_domain}(?:\\x2e#{sub_domain})*"
			local_part = "#{word}(?:\\x2e#{word})*"
			addr_spec = "#{local_part}\\x40#{domain}"
			/\A#{addr_spec}\z/n
		end

		# Pattern for (loosely) matching a valid hostname. This isn't strictly RFC-compliant
		# because, in practice, many hostnames used on the Internet aren't.
		RFC1738_HOSTNAME = begin
			alphadigit = /[a-z0-9]/i
			# toplabel		 = alpha | alpha *[ alphadigit | "-" ] alphadigit
			toplabel = /[a-z]((#{alphadigit}|-)*#{alphadigit})?/i
			# domainlabel	 = alphadigit | alphadigit *[ alphadigit | "-" ] alphadigit
			domainlabel = /#{alphadigit}((#{alphadigit}|-)*#{alphadigit})?/i
			# hostname		 = *[ domainlabel "." ] toplabel
			hostname = /\A(#{domainlabel}\.)*#{toplabel}\z/
		end

		# The Hash of builtin constraints that are validated against a regular
		# expression.
		# :TODO: Document that these are the built-in constraints that can be used in a route
		BUILTIN_CONSTRAINT_PATTERNS = {
			:boolean	  => /^(?<boolean>t(?:rue)?|y(?:es)?|[10]|no?|f(?:alse)?)$/i,
			:integer	  => /^(?<integer>[\-\+]?\d+)$/,
			:float		  => /^(?<float>[\-\+]?(?:\d*\.\d+|\d+)(?:e[\-\+]?\d+)?)$/i,
			:alpha		  => /^(?<alpha>[[:alpha:]]+)$/,
			:alphanumeric => /^(?<alphanumeric>[[:alnum:]]+)$/,
			:printable	  => /\A(?<printable>[[:print:][:blank:]\r\n]+)\z/,
			:string		  => /\A(?<string>[[:print:][:blank:]\r\n]+)\z/,
			:word		  => /^(?<word>[[:word:]]+)$/,
			:email		  => /^(?<email>#{RFC822_EMAIL_ADDRESS})$/,
			:hostname	  => /^(?<hostname>#{RFC1738_HOSTNAME})$/,
			:uri		  => /^(?<uri>#{URI::URI_REF})$/,
			:uuid		  => /^(?<uuid>[[:xdigit:]]{8}(?:-[[:xdigit:]]{4}){3}-[[:xdigit:]]{12})$/i,
			:date         => /.*\d.*/,
		}

		# Field values which result in a valid ‘true’ value for :boolean constraints
		TRUE_VALUES = %w[t true y yes 1]


		### Return true if name is the name of a built-in constraint.
		def self::valid?( name )
			return BUILTIN_CONSTRAINT_PATTERNS.key?( name.to_sym )
		end


		### Create a new BuiltinConstraint using the pattern named name for the specified field.
		def initialize( field, name, *options, &block )
			name ||= field
			@pattern_name = name
			pattern = BUILTIN_CONSTRAINT_PATTERNS[ name.to_sym ] or
				raise ScriptError, "no such builtin constraint %p" % [ name ]

			super( field, pattern, *options, &block )
		end


		######
		public
		######

		# The name of the builtin pattern the field should be constrained by
		attr_reader :pattern_name


		### Check for an additional post-processor method, and if it exists, return it as
		### a Method object.
		def block
			if custom_block = super
				return custom_block
			else
				post_processor = "post_process_%s" % [ @pattern_name ]
				return nil unless self.respond_to?( post_processor )
				return self.method( post_processor )
			end
		end


		### Return the constraint expressed as a String.
		def validator_description
			return "a '%s'" % [ self.pattern_name ]
		end


		#########
		protected
		#########

		### Post-process a :boolean value.
		def post_process_boolean( val )
			return TRUE_VALUES.include?( val.to_s.downcase )
		end


		### Constrain a value to a parseable Date
		def post_process_date( val )
			return Date.parse( val )
		rescue ArgumentError
			return nil
		end


		### Constrain a value to a Float
		def post_process_float( val )
			return Float( val.to_s )
		end


		### Post-process a valid :integer field.
		def post_process_integer( val )
			return Integer( val.to_s )
		end


		### Post-process a valid :uri field.
		def post_process_uri( val )
			return URI.parse( val.to_s )
		rescue URI::InvalidURIError => err
			self.log.error "Error trying to parse URI %p: %s" % [ val, err.message ]
			return nil
		rescue NoMethodError
			self.log.debug "Ignoring bug in URI#parse"
			return nil
		end

	end # class BuiltinConstraint



	#################################################################
	### I N S T A N C E	  M E T H O D S
	#################################################################

	### Create a new Strelka::ParamValidator object.
	def initialize
		@constraints = {}
		@fields      = {}
		@untaint_all = false

		self.reset
	end


	### Copy constructor.
	def initialize_copy( original )
		fields       = deep_copy( original.fields )
		self.reset
		@fields      = fields
		@constraints = deep_copy( original.constraints )
	end


	######
	public
	######

	# The constraints hash
	attr_reader :constraints

	# The Hash of raw field data (if validation has occurred)
	attr_reader :fields

	##
	# Global untainting flag
	attr_predicate_accessor :untaint_all?
	alias_method :untaint_all_constraints=, :untaint_all=
	alias_method :untaint_all_constraints?, :untaint_all?

	##
	# Returns +true+ if the paramvalidator has been given parameters to validate. Adding or
	# overriding constraints resets this.
	attr_predicate_accessor :validated?


	### Reset the validation state.
	def reset
		self.log.debug "Resetting validation state."
		@validated     = false
		@valid         = {}
		@parsed_params = nil
		@missing       = []
		@unknown       = []
		@invalid       = {}
	end


	### :call-seq:
	###	   add( name, *flags )
	###	   add( name, constraint, *flags )
	###	   add( name, description, *flags )
	###	   add( name, constraint, description, *flags )
	###
	### Add a validation for a parameter with the specified +name+. The +args+ can include
	### a constraint, a description, and one or more flags.
	def add( name, *args, &block )
		name = name.to_sym
		constraint = Constraint.for( name, *args, &block )

		# No-op if there's already a parameter with the same name and constraint
		if self.constraints.key?( name )
			return if self.constraints[ name ] == constraint
			raise ArgumentError,
				"parameter %p is already defined as %s; perhaps you meant to use #override?" %
					[ name.to_s, self.constraints[name] ]
		end

		self.log.debug "Adding parameter %p: %p" % [ name, constraint ]
		self.constraints[ name ] = constraint

		self.validated = false
	end


	### Replace the existing parameter with the specified name. The args replace the
	### existing description, constraints, and flags. See #add for details.
	def override( name, *args, &block )
		name = name.to_sym
		raise ArgumentError,
			"no parameter %p defined; perhaps you meant to use #add?" % [ name.to_s ] unless
			self.constraints.key?( name )

		self.log.debug "Overriding parameter %p" % [ name ]
		self.constraints[ name ] = Constraint.for( name, *args, &block )

		self.validated = false
	end


	### Return the Array of parameter names the validator knows how to validate (as Strings).
	def param_names
		return self.constraints.keys.map( &:to_s ).sort
	end


	### Stringified description of the validator
	def to_s
	    "%d parameters (%d valid, %d invalid, %d missing)" % [
	        self.fields.size,
	        self.valid.size,
	        self.invalid.size,
	        self.missing.size,
	    ]
	end


	### Return a human-readable representation of the validator, suitable for debugging.
	def inspect
		required, optional = self.constraints.partition do |_, constraint|
			constraint.required?
		end

		return "#<%p:0x%016x %s, profile: [required: %s, optional: %s] global untaint: %s>" % [
			self.class,
			self.object_id / 2,
			self.to_s,
			required.empty? ? "(none)" : required.map( &:last ).map( &:name ).join(','),
			optional.empty? ? "(none)" : optional.map( &:last ).map( &:name ).join(','),
			self.untaint_all? ? "enabled" : "disabled",
		]
	end


	### Hash of field descriptions
	def descriptions
		return self.constraints.each_with_object({}) do |(field,constraint), hash|
			hash[ field ] = constraint.description
		end
	end


	### Set field descriptions en masse to new_descs.
	def descriptions=( new_descs )
		new_descs.each do |name, description|
			raise NameError, "no parameter named #{name}" unless
				self.constraints.key?( name.to_sym )
			self.constraints[ name.to_sym ].description = description
		end
	end


	### Get the description for the specified +field+.
	def get_description( field )
		constraint = self.constraints[ field.to_sym ] or return nil
		return constraint.description
	end


	### Validate the input in +params+. If the optional +additional_constraints+ is
	### given, merge it with the validator's existing constraints before validating.
	def validate( params=nil, additional_constraints=nil )
		self.log.debug "Validating."
		self.reset

		# :TODO: Handle the additional_constraints

		params ||= @fields
		params = stringify_keys( params )
		@fields = deep_copy( params )

		# Use the constraints list to extract all the parameters that have corresponding
		# constraints
		self.constraints.each do |field, constraint|
			self.log.debug "  applying %s to any %p parameter/s" % [ constraint, field ]
			value = params.delete( field.to_s )
			self.log.debug "  value is: %p" % [ value ]
			self.apply_constraint( constraint, value )
		end

		# Any left over are unknown
		params.keys.each do |field|
			self.log.debug "  unknown field %p" % [ field ]
			@unknown << field
		end

		@validated = true
	end


	### Apply the specified +constraint+ (a Strelka::ParamValidator::Constraint object) to
	### the given +value+, and add the field to the appropriate field list based on the
	### result.
	def apply_constraint( constraint, value )
		if value
			result = constraint.apply( value, self.untaint_all? )

			if !result.nil?
				self.log.debug "  constraint for %p passed: %p" % [ constraint.name, result ]
				self[ constraint.name ] = result
			else
				self.log.debug "  constraint for %p failed" % [ constraint.name ]
				@invalid[ constraint.name.to_s ] = value
			end
		elsif constraint.required?
			self.log.debug "  missing parameter for %p" % [ constraint.name ]
			@missing << constraint.name.to_s
		end
	end


	### Clear existing validation information, merge the specified +params+ with any existing
	### raw fields, and re-run the validation.
	def revalidate( params={} )
		merged_fields = self.fields.merge( params )
		self.reset
		self.validate( merged_fields )
	end


	## Fetch the constraint/s that apply to the parameter named name as a Regexp, if possible.
	def constraint_regexp_for( name )
		self.log.debug "  searching for a constraint for %p" % [ name ]

		# Fetch the constraint's regexp
		constraint = self.constraints[ name.to_sym ]
		raise ScriptError,
			"can't route on a parameter with a %p" % [ constraint.class ] unless
			constraint.respond_to?( :pattern )

		re = constraint.pattern
		self.log.debug "  bounded constraint is: %p" % [ re ]

		# Unbind the pattern from beginning or end of line.
		# :TODO: This is pretty ugly. Find a better way of modifying the regex.
		re_str = re.to_s.
			sub( %r{\(\?[\-mix]+:(.*)\)}, '\1' ).
			gsub( PARAMETER_PATTERN_STRIP_RE, '' )
		self.log.debug "  stripped constraint pattern down to: %p" % [ re_str ]

		return Regexp.new( "(?<#{name}>#{re_str})", re.options )
	end


	### Returns the valid fields after expanding Rails-style
	### 'customer[address][street]' variables into multi-level hashes.
	def valid
		self.validate unless self.validated?

		unless @parsed_params
			@parsed_params = {}
			for key, value in @valid
				value = [ value ] if key.to_s.end_with?( '[]' )
				if key.to_s.include?( '[' )
					build_deep_hash( value, @parsed_params, get_levels(key.to_s) )
				else
					@parsed_params[ key ] = value
				end
			end
		end

		return @parsed_params
	end


	### Index fetch operator; fetch the validated (and possible parsed) value for
	### form field +key+.
	def []( key )
		return @valid[ key.to_sym ]
	end


	### Index assignment operator; set the validated value for form field +key+
	### to the specified +val+.
	def []=( key, val )
		@parsed_params = nil
		return @valid[ key.to_sym ] = val
	end


	### Returns +true+ if there were no arguments given.
	def empty?
		return self.fields.empty?
	end


	### Returns +true+ if there were arguments given.
	def args?
		return !self.fields.empty?
	end
	alias_method :has_args?, :args?


	### The names of fields that were required, but missing from the parameter list.
	def missing
		self.validate unless self.validated?
		return @missing
	end


	### The Hash of fields that were present, but invalid (didn't match the field's constraint)
	def invalid
		self.validate unless self.validated?
		return @invalid
	end


	### The names of fields that were present in the parameters, but didn't have a corresponding
	### constraint.
	def unknown
		self.validate unless self.validated?
		return @unknown
	end


	### Returns +true+ if any fields are missing or contain invalid values.
	def errors?
		return !self.okay?
	end
	alias_method :has_errors?, :errors?


	### Return +true+ if all required fields were present and all present fields validated
	### correctly.
	def okay?
		return (self.missing.empty? && self.invalid.empty?)
	end


	### Return an array of field names which had some kind of error associated
	### with them.
	def error_fields
		return self.missing | self.invalid.keys
	end


	### Return an error message for each missing or invalid field; if
	### +includeUnknown+ is +true+, also include messages for unknown fields.
	def error_messages( include_unknown=false )
		msgs = []

		msgs += self.missing_param_errors + self.invalid_param_errors
		msgs += self.unknown_param_errors if include_unknown

		return msgs
	end


	### Return an Array of error messages, one for each field missing from the last validation.
	def missing_param_errors
		return self.missing.collect do |field|
			constraint = self.constraints[ field.to_sym ] or
				raise NameError, "no such field %p!" % [ field ]
			"Missing value for '%s'" % [ constraint.description ]
		end
	end


	### Return an Array of error messages, one for each field that was invalid from the last
	### validation.
	def invalid_param_errors
		return self.invalid.collect do |field, _|
			constraint = self.constraints[ field.to_sym ] or
				raise NameError, "no such field %p!" % [ field ]
			"Invalid value for '%s'" % [ constraint.description ]
		end
	end


	### Return an Array of error messages, one for each field present in the parameters in the last 
	### validation that didn't have a constraint associated with it.
	def unknown_param_errors
		self.log.debug "Fetching unknown param errors for %p." % [ self.unknown ]
		return self.unknown.collect do |field|
			"Unknown parameter '%s'" % [ field.capitalize ]
		end
	end


	### Return a new ParamValidator with the additional +params+ merged into
	### its values and re-validated.
	def merge( params )
		copy = self.dup
		copy.merge!( params )
		return copy
	end


	### Merge the specified +params+ into the receiving ParamValidator and
	### re-validate the resulting values.
	def merge!( params )
		return if params.empty?
		self.log.debug "Merging parameters for revalidation: %p" % [ params ]
		self.revalidate( params )
	end


	### Returns an array containing valid parameters in the validator corresponding to the
	### given +selector+(s).
	def values_at( *selector )
		selector.map!( &:to_sym )
		return self.valid.values_at( *selector )
	end



	#######
	private
	#######

	### Build a deep hash out of the given parameter +value+
	def build_deep_hash( value, hash, levels )
		if levels.length == 0
			value.untaint
		elsif hash.nil?
			{ levels.first => build_deep_hash(value, nil, levels[1..-1]) }
		else
			hash.update({ levels.first => build_deep_hash(value, hash[levels.first], levels[1..-1]) })
		end
	end


	### Get the number of hash levels in the specified +key+
	### Stolen from the CGIMethods class in Rails' action_controller.
	def get_levels( key )
		all, main, bracketed, trailing = PARAMS_HASH_RE.match( key ).to_a
		if main.nil?
			return []
		elsif trailing
			return [key.untaint]
		elsif bracketed
			return [main.untaint] + bracketed.slice(1...-1).split('][').collect {|k| k.untaint }
		else
			return [main.untaint]
		end
	end

end # class Strelka::ParamValidator