#--
# *** This code is copyright 2004 by Gavin Kistner
# *** It is covered under the license viewable at http://phrogz.net/JS/_ReuseLicense.txt
# *** Reuse or modification is free provided you abide by the terms of that license.
# *** (Including the first two lines above in your source code usually satisfies the conditions.)
#++
# Classes for working with ValidForm/ValidForm::Field objects of all types, specifying their labels, values, and validation rules.
#
# The ValidForm library allows you to specify a form fully in Ruby code, along with validation rules for that data.
# You can then easily emit an HTML version of that form (either fully--with a single method call--or in controlled pieces) which includes the hooks needed for client-side validation using FormAutoValidate[http://phrogz.net/tmp/FormAutoValidate/formautovalidate_docs.html].
# Because client-side isn't enough, you can then re-validate the data server-side in Ruby (again with a single method call to pull the submitted values into the form and validate them).
# And finally, re-emitting the html for an already-validated form emits CSS hooks and validation error messages.
#
# ValidFormTest.tar.gz[http://phrogz.net/RubyLibs/ValidFormTest.tar.gz] is a fully-functioning mod_ruby/eRuby example, with files showing how to:
# * Create a complex form, with various validation rules.
# * Emit the HTML for that form.
# * Use CSS to style the semantic markup in non-trivial ways. <i>(At least for Safari/Mozilla...I have no idea if IEWin supports this particular example properly.)</i>
# * View the client-side library in action.
# * Load the values back into the form server-side, re-validate, and re-emit new HTML noting the errors.
#
# One caveat: the hooks needed for FormAutoValidate produce 'invalid' HTML; that is, the output will no longer validate as HTML4 because of the custom attributes put into the fields. This should cause no problems for the browser, as in all other ways the output should be HTML4Strict compliant.
#
# Include link:../ValidForm_html.rb to be able to transform these fields into HTML equivalent.
#
# Relies on basiclibrary.rb for WriteOnceHash and Time#custom_format
#
# Author::     Gavin Kistner  (mailto:!@phrogz.net)
# Copyright::  Copyright (c)2004 Gavin Kistner
# License::    See http://Phrogz.net/JS/_ReuseLicense.txt for details
# Version::    1.1
# Full Code::  link:../ValidForm.rb

require "basiclibrary"
require "time"

class ValidForm
end


##########################################################################################
# A wrapper module for methods shared by ValidForm, ::Fieldset, and ::OptionSet.
##########################################################################################
module ValidForm::Container

	# All fields in this container as an Array; some 'fields' may be sub-containers. See also #everyField
	attr_reader(:fields)

	# Every class that includes ::Container should implement a #name method.
	attr_reader(:name)
	
	# Returns the contained field with +id+==_idOrName_; if none exists, looks for and returns an array of fields where +name+==_idOrName_; if neither exist, returns +nil+.
	def [](idOrName)
		fieldById(idOrName) || fieldsByName(idOrName)
	end
	
	# Add one or more fields or field containers (fieldsets) to the container.
	# Returns a reference to the receiver, for simple chaining and nesting:
	#
	#   search = ValidForm.new('search.rhtml','GET')
	#   search.add_fields(
	#   	ValidForm::Text.new('q','q',nil,'Find:'),
	#   	ValidForm::OptionSet.new('sections',nil,nil,'Look In:').add_fields(
	#   		ValidForm::Option.new('s1','slashdot.org','/.'),
	#   		ValidForm::Option.new('s2','google.com','Google'),
	#   		ValidForm::Option.new('s3','apple.com','Apple')
	#      )
	#   )
	#
	# Note that the +id+ for each item added must be unique, or an error will be raised.
	
	def add_fields(*fieldsOrContainers)
		@fieldsById=WriteOnceHash.new unless @fieldsById
		@fieldsByName=WriteOnceHash.new unless @fieldsByName
		@fields=[] unless @fields

		fieldsOrContainers.each{ |fs|
			raise ArgumentError,"An illegal class (#{fs.class}) was passed to add_fields; only the following classes are allowed: #{@allowed_container_classes.join(',')}" if @allowed_container_classes && (@allowed_container_classes & fs.class.ancestors).empty?
			raise ArgumentError,"An illegal class (#{fs.class}) was passed to add_fields; the following classes may not be added: #{@disallowed_container_classes.join(',')}" if @disallowed_container_classes && !((@disallowed_container_classes & fs.class.ancestors).empty?)
			raise ArgumentError,"You tried to add a container to itself. Are you trying to kill us all?" if fs==self
			fs.parent_container=self if fs.respond_to?(:parent_container=)
			@fieldsById[fs.id] = fs if fs.id
			if fs.name
				if !@fieldsByName[fs.name]
					@fieldsByName[fs.name] = [fs]
				else
					@fieldsByName[fs.name] << fs
				end
			end
			fs.everyField(true).each{ |f|
				@fieldsById[f.id] = f if f.id
				if f.name
					if !@fieldsByName[f.name]
						@fieldsByName[f.name] = [f]
					else
						@fieldsByName[f.name] << f
					end
				end
			} if fs.respond_to?(:everyField)
		}
		@fields.push(*fieldsOrContainers)
		@parent_container.subcontainer_changed if @parent_container
		self
	end

	# Return the field in the container with +id+==_id_. If no such field exists, +nil+ is returned.
	#
	# See also #fieldsByName and #[].
	def fieldById(id)
		@fieldsById[id]
	end

	# An array of all fields in this container as a flat Array, whether directly in this container or sub-containers.
	# Pass +true+ for _includeContainers_ to have subcontainers themselves included in addition to the fields they contain.
	#
	#   myForm = ValidForm.new().add_fields(
	#   	foo=ValidForm::Field.new('foo'),
	#   	bar=ValidForm::Fieldset.new('bar').add_fields(
	#   		jim=ValidForm::Field.new('jim'),
	#   		jam=ValidForm::Field.new('jam')
	#   	)
	#   )
	#
	#   myForm.fields.each{ |f| print f.id,',' }
	#     => 'foo,bar,'
	#   myForm.everyField.each{ |f| print f.id,',' }
	#     => 'foo,jim,jam,'
	#   myForm.everyField(true).each{ |f| print f.id,',' }
	#     => 'foo,bar,jim,jam,'
	def everyField( includeContainers=false )
		includeContainers=!!includeContainers
		fs=[]
		@fields.each{ |f|
			if f.respond_to?(:everyField)
				fs << f if includeContainers
				fs.concat(f.everyField(includeContainers))
			else
				fs << f
			end
		} if @fields
		fs
	end

	# Returns the parent container for the receiving container; +nil+ if none exists.
	def parent_container
		@parent_container
	end


	# Returns an Array of all fields in the container with +name+==_name_, whether they are in the top-level container (form) or a sub-container (fieldset).
	# If no fields with _name_ exist, +nil+ will be returned.
	#
	# See also #fieldById and #[].
	def fieldsByName(name)
		@fieldsByName[name]
	end

	# Sets which classes of items are allowed inside this container. By default, any type of object may be passed to #add_fields.
	#
	# <i>(This method is used by classes including this module; you should never need to invoke it directly.</i>
	#
	#   class ValidForm
	#   	include FieldContainer
	#   	def initialize
	#   		#...
	#   		allowed_container_classes( ValidForm::Fieldset, ValidForm::Field )
	#   	end
	#   end
	def allowed_container_classes(*fieldClasses)
		fieldClasses.each{ |fc|
			raise ArgumentError,"An argument other than a Class or Module was passed to ValidForm::Container#allowed_container_classes." unless fc.is_a?(Module)
		}
		@allowed_container_classes=fieldClasses
	end

	def disallowed_container_classes(*fieldClasses)
		fieldClasses.each{ |fc|
			raise ArgumentError,"An argument other than a Class or Module was passed to ValidForm::Container#disallowed_container_classes." unless fc.is_a?(Module)
		}
		@disallowed_container_classes=fieldClasses
	end

	def parent_container=(c) #:nodoc:
		if @parent_container && @parent_container!=f
			raise ArgumentError,'ValidForm::Fieldset added to a form, but was already in one.'
		else
			@parent_container=c
		end
	end

	# Recalculate everything because an item was added to an existing subcontainer and I'm too lazy to figure out an efficient way to deal with it.
	def subcontainer_changed #:nodoc:
		@fieldsById=WriteOnceHash.new
		@fieldsByName=WriteOnceHash.new
		fs = everyField(true)
		fs.each{ |f|
			@fieldsById[f.id] = f if fs.id
			if f.name
				if !@fieldsByName[f.name]
					@fieldsByName[f.name] = [f]
				else
					@fieldsByName[f.name] << f
				end
			end
		}
	end

end







##########################################################################################
# Represents a related set of ::Field and ::Fieldset objects, along with an +action+
# (e.g. page that should process the form) and a +method+ (e.g. "GET" or "POST" for html).
##########################################################################################
class ValidForm
	include ValidForm::Container

	# _action_::     The action to take when processing the form. (e.g. "<tt>process.rbx</tt>" for HTML)
	# _method_::     The method to use to submit the form.  (e.g. "+GET+" or "+POST+" for HTML)
	# _attributes_:: A Hash of various custom attributes to be associated with the form.
	#
	# Create a new ValidForm object. Call ValidForm#addField to add content to the form.
	#
	#   loginForm = ValidForm.new('userlogin.rbx','POST')
	#   loginForm.add_fields(
	#   	ValidForm::Text.new('userName',nil,nil,'Username:'),
	#   	ValidForm::Password.new('password',nil,nil,'Password:'),
	#   	ValidForm::Submit.new('fsubmit',nil,nil,'Login to the Site')
	#   )
	#
	# See also ::Container#add_fields.
	def initialize(action=nil,method="POST",attributes={})
		@attributes=attributes || {}
		raise ArgumentError,"The attributes parameter passed to ValidForm::Fieldset#new was not a Hash" unless @attributes.is_a?(Hash)
		@action=action
		@method=method
		allowed_container_classes(ValidForm::Container,ValidForm::Field)
		disallowed_container_classes(ValidForm)
	end

	# The action to take when processing the form.
	attr_accessor(:action)

	# The method to use to submit the form.
	attr_accessor(:method)

	# A Hash of various custom attributes to be associated with the form.
	attr_accessor(:attributes)


	# Checks each field in the form to see if it is valid. (See ValidForm::Field#validate).
	# Returns +nil+ if every field is valid; otherwise it returns an Array of all invalid fields,
	# each field having a non-+nil+ ValidForm::Field#errors array.
	def validate
		invalids=[]
		everyField(true).each{ |f|
			invalids << f if f.respond_to?(:validate) && f.validate!=true
		}
		invalids.empty? ? nil : invalids
	end
end











##########################################################################################
# Represents a logical grouping of fields in a ValidForm.
# <i>(In HTML, this would be represented by a <tt><fieldset></tt> element.)</i>
# All fields in a form are placed into a fieldset; when fields are added directly to a
# form via ::Container#add_fields, an anonymous fieldset is created to hold them.
##########################################################################################
class ValidForm::Fieldset
	include ValidForm::Container

	# The human-friendly label for this fieldset. <em>(Used for the <tt><legend></tt> in HTML)</em>
	attr_accessor(:label)

	# The unique identifier for this fieldset.
	attr_reader(:id)

	# An Array of every field added to this fieldset. See also ::Container#fieldsByName and ::Container#fieldById
	attr_reader(:fields)

	# The form that this fieldset is in. Use <tt>myForm.addFields( ... )</tt> to add a fieldset to a form. <i>(See ::Container#add_fields).</i>
	attr_reader(:form)

	# _id_::         The unique identifier for this fieldset.
	# _label_::      The human-friendly label for the fieldset.
	# _attributes_:: A Hash of various custom attributes to be associated with the fieldset.
	#
	#  contact = ValidForm.new('updatecontact.rbx','POST')
	#  phones  = ValidForm::Fieldset.new('phones','Phone Numbers')
	#
	#  contact.add_fields( phones, ValidForm::Field.new('phone_home',nil,nil,'Home') )
	#  contact.add_fields( phones, ValidForm::Field.new('phone_work',nil,nil,'Work') )
	#  contact.add_fields( phones, ValidForm::Field.new('phone_cell',nil,nil,'Cell') )
	def initialize(id=nil,label=nil,attributes={})
		@attributes=attributes || {}
		raise ArgumentError,"The attributes parameter passed to ValidForm::Fieldset#new was not a Hash" unless @attributes.is_a?(Hash)
		@id=id
		@label=label
		allowed_container_classes(ValidForm::Fieldset,ValidForm::Field)
	end
end










##########################################################################################
# A generic ValidForm field. See also the subclasses ::Text, ::Password, ::Option,
# ::OptionSet, ::RadioSet, ::CheckboxSet, ::Reset, and ::Submit
##########################################################################################
class ValidForm::Field

	# Complete list of valid keys; used to ensure that validationRules are valid.
	VALIDATION_KEYS = { :required=>1,:minlength=>1,:maxlength=>1,:type=>1,:match=>1,:nomatch=>1,:minvalue=>1,:maxvalue=>1,:reqfail_msg=>1,:typefail_msg=>1 }

	# The unique identifier for this field.
	attr_reader(:id)

	# The name associated with this field.
	attr_accessor(:name)

	# The current value of this field.
	attr_accessor(:value)

	# The human-friendly label for this field.
	attr_accessor(:label)

	# A Hash of additional attributes to associate with this field.
	attr_accessor(:attributes)

	# A Hash of validation rules which apply to this field.
	# See #validate for more information.
	attr_accessor(:validationRules)

	# _id_::          	  The unique identifier for this field. [reqd]
	# _name_::        	  The name associated with this field (used for name/value pairs).
	# _value_::       	  The initial String value for the field.
	# _label_::       	  The human-friendly label for the field.
	# _attributes_:: 	  A Hash of various custom attributes to be associated with the field.
	# _validationRules_:: A Hash of validation rules which apply to this field.
	#                     See #validate for more information.
	#
	#   email = ValidForm::Field.new('email','email',nil,'Your email address')
	#   email.validationRules[:required]=true
	#   email.validationRules[:reqfail_msg]="Please provide your email " +
	#                                       "address so we can spam you!"
	#
	#   story = ValidForm::Text.new('story','story',nil,'Your email address',
	#           	{:cols=>80, :rows=> 10}
	#           	{:minlength=>50, :maxlength=>2000}
	#           )
	#   story.multiline=true
	def initialize(id,name=nil,value=nil,label=nil,attributes={}, validationRules={})
		@id=id
		@name=name
		@value=value
		@label=label
		@form=nil
		@attributes=attributes || {}
		@validationRules=validationRules || {}
		@errors=false
		@validated=false
		raise ArgumentError,"No id parameter to ValidForm::Field#new" unless @id && @id!=''
		raise ArgumentError,"Non-hash passed as attributes parameter to ValidForm::Field#new" unless @attributes.is_a?(Hash)
		raise ArgumentError,"Non-hash passed as validationRules parameter to ValidForm::Field#new" unless @validationRules.is_a?(Hash)
		@validationRules.each_key{ |k|
			raise ArgumentError,"Key '#{k}' passed in validationRules is not a known validation key" unless VALIDATION_KEYS[k]
		}
	end

	# Checks the field's +value+ against the +validationRules+ set for the field.
	# The following validation keys apply to every type of field:
	# +required+::  A truth value representing whether or not the value may be +nil+ or <tt>""</tt>. If +required+ is +true+ and the +value+ is +nil+ or an empty string, the validation fails.
	# +minlength+:: An Integer representing the minimum number of characters allowed for the value (if it has a value).
	# +maxlength+:: An Integer representing the maximum number of characters allowed for the value.
	# +type+::      One of the following Symbols: <tt>:phone</tt>, <tt>:email</tt>, <tt>:zipcode</tt>, <tt>:integer</tt>, <tt>:float</tt>, <tt>:date</tt>, <tt>:time</tt>, <tt>:datetime</tt>.
	# +match+::     A Regexp which will be matched against the value; if a match is *not* found, the validation fails.
	# +nomatch+::   A Regexp which will be matched against the value; if a match *is* found, the validation fails.
	# +minvalue+::  A Number (for <tt>:type=>:integer</tt> or <tt>:type=>:float</tt>) or Time (for <tt>:type=>:date</tt>, <tt>:type=>:time</tt>, <tt>:type=>:datetime</tt>) which specifies the minimum legal value.
	# +maxvalue+::  A Number or Time which specifies the maximum legal value.
	#
	# See also ::OptionSet#validate for the additional +minchosen+ and +maxchosen+ validation rules applicable to option sets.
	#
	# In addition to the above, the following strings can be set in the +validationRules+ Hash to customize certain error messages:
	# +reqfail_msg+::  The message to display if the field is required, but was not filled out.
	# +typefail_msg+:: The message to display if the field has a +type+, +match+, or +nomatch+ validation set and fails.
	# In the above message strings, instances of the token <tt>%label%</tt> will be replaced by the +label+ for the field. (If the field has no +label+, its +name+ will be used; if it has no +name+, its +id+ will be used.)
	#
	# Returns +true+ if the field validates successfully; +false+ if one or more validation rules are broken. (If the field fails to validate properly, the errors will be stored as an Array of instances of ::ValidationError, accessible via #errors.)
	def validate
		@errors=[]
		rs = @validationRules

		val=self.value()

		dRE = '(?:1[012]|0?\d)/(?:[012]?\\d|3[01])/(?:\d{2}|\d{4})'
		tRE = '(?:(?:[01]?\d|2[0-3]):[0-5]\d(?::[0-5]\d)?|(?:0?\d|1[0-2])(?::[0-5]\d){1,2}\s*[ap]\.?m\.?)'
		dateRE = Regexp.new('^'+dRE+'$',true)
		timeRE = Regexp.new('^'+tRE+'$',true)
		datetimeRE = Regexp.new('^(?:'+dRE+'\\s+'+tRE+'|'+tRE+'\\s+'+dRE+')$',true)

		if val==nil || val==""
			@errors << ValidForm::ValidationError.new(self,ValidForm::ValidationError::REQUIRED_VALUE_EMPTY) if rs[:required]
		else
			@errors << ValidForm::ValidationError.new(self,ValidForm::ValidationError::LENGTH_TOO_SMALL) if rs[:minlength] && val.length<rs[:minlength]
			@errors << ValidForm::ValidationError.new(self,ValidForm::ValidationError::LENGTH_TOO_LARGE) if rs[:maxlength] && val.length>rs[:maxlength]
			case rs[:type]
				when :phone
					rs[:match]=/^\D*\d*\D*(\d{3})?\D*\d{3}\D*\d{4}\D*$/
					failType=ValidForm::ValidationError::MATCH_FAIL_PHONE
				when :email
					rs[:match]=/^[^@]+@[^.]+\..+$/
					failType=ValidForm::ValidationError::MATCH_FAIL_EMAIL
				when :zipcode
					rs[:match]=/^\d{5}(?:-\d{4})?$/
					failType=ValidForm::ValidationError::MATCH_FAIL_ZIPCODE
				when :integer
					rs[:match]=/^-?\d+$/
					failType=ValidForm::ValidationError::MATCH_FAIL_INTEGER
				when :float
					rs[:match]=/^-?(?:\d+|\d*\.\d+)$/
					failType=ValidForm::ValidationError::MATCH_FAIL_FLOAT
				when :date
					rs[:match]=dateRE
					failType=ValidForm::ValidationError::MATCH_FAIL_DATE
				when :time
					rs[:match]=timeRE
					failType=ValidForm::ValidationError::MATCH_FAIL_TIME
				when :datetime
					rs[:match]=datetimeRE
					failType=ValidForm::ValidationError::MATCH_FAIL_DATETIME
				else
					failType=ValidForm::ValidationError::MATCH_FAIL_CUSTOM
			end
			if (rs[:match] && !rs[:match].match(val)) || (rs[:nomatch] && rs[:nomatch].match(val))
				@errors << ValidForm::ValidationError.new(self,failType)
			else
				case rs[:type]
					when :integer,:float
						@errors << ValidForm::ValidationError.new(self,ValidForm::ValidationError::VALUE_TOO_SMALL) if rs[:minvalue] && val.to_f<rs[:minvalue]
						@errors << ValidForm::ValidationError.new(self,ValidForm::ValidationError::VALUE_TOO_LARGE) if rs[:maxvalue] && val.to_f>rs[:maxvalue]
					when :date,:time,:datetime
						t = val.is_a?(Time) ? val : Time.parse(val)
						rs[:minvalue] = (rs[:minvalue].is_a?(Time) ? rs[:minvalue] : Time.parse(rs[:minvalue])) if rs[:minvalue]
						rs[:maxvalue] = (rs[:maxvalue].is_a?(Time) ? rs[:maxvalue] : Time.parse(rs[:maxvalue])) if rs[:maxvalue]
						@errors << ValidForm::ValidationError.new(self,ValidForm::ValidationError::DATETIME_TOO_SMALL) if rs[:minvalue] && t < rs[:minvalue]
						@errors << ValidForm::ValidationError.new(self,ValidForm::ValidationError::DATETIME_TOO_LARGE) if rs[:maxvalue] && t > rs[:maxvalue]
				end
			end
		end

		@validated=true
		@errors=nil if @errors.empty?
		@errors || true
	end

	def devalidate #:nodoc:
		@errors=false
		@validated=false
	end

	# Returns +true+ if the field has been validated against the current value; +false+ otherwise.
	#
	# Note that <tt>validated?==true</tt> only indicates that validation has occurred; the validation may have failed, in which case #error would have a non-+nil+ value.
	def validated?
		@validated
	end

	# Returns +false+ if the field has not been validated against the current +value+; +nil+ if the field has been validated successfully, or an array of one or more ::ValidationError objects otherwise.
	def errors
		@errors
	end

	# :stopdoc:
	def value=(v)
		devalidate
		@value=v
	end
	
	def name=(n)
		#ToDo: reset Container's name
		@name=n
	end
	# :startdoc:
end






##########################################################################################
# Represents a form field for text input.
# <i>(May represent a single-line of input or an extended field for multiline input.)</i>
#
# See ::Field#new for details on the parameters to pass for a constructor.
#   story = ValidForm::Text.new('story','story',nil,'Your sob story:',
#           	{:cols=>80, :rows=>10},
#           	{:minlength=>50, :maxlength=>2000, :required=>true}
#           )
#   story.multiline=true
#
# For convenient inline creation, the ::Text object will set the multiline attribute to true
# if you put the symbol/value pair <tt>:multiline=>true</tt> in the +attributes+ Hash <b>during
# creation</b>. (The pair will then be removed from the +attributes+ Hash.)
#
#   story = ValidForm::Text.new('story',nil,nil,'Your sob story:',
#           	{:cols=>80, :rows=>10, :multiline=>true },
#           	{:minlength=>50, :maxlength=>2000, :required=>true }
#           )
#   story.multiline  => true
#   story.attributes => {:cols=>80, :rows=>10}
# See also ::Password.
##########################################################################################
class ValidForm::Text < ValidForm::Field

	# Does this field allow more than one line of text? +false+ by default.
	attr_accessor(:multiline)

	def initialize(*params) #:nodoc:
		super
		@multiline = !!@attributes[:multiline]
		@attributes.delete(:multiline)
	end
end


##########################################################################################
# Represents a single-line ::Text field for entering passwords.
# +multiline+ is +false+ and may not be changed.
#
# See ::Field#new for details on the parameters to pass for a constructor.
##########################################################################################
class ValidForm::Password < ValidForm::Text
	undef_method(:multiline=)
end

##########################################################################################
# Represents an input whose values should not be displayed to the user.
#
# See ::Field#new for details on the parameters to pass for a constructor.
##########################################################################################
class ValidForm::Hidden < ValidForm::Text
end





##########################################################################################
# Represents a button used to submit the form.
#
# <i>(See ::Field#new for a description of the constructor.)</i>
##########################################################################################
class ValidForm::Submit < ValidForm::Field
end


##########################################################################################
# Represents a button used to reset the form to its default values.
#
# <i>(See ::Field#new for a description of the constructor.)</i>
##########################################################################################
class ValidForm::Reset < ValidForm::Field
end



##########################################################################################
# A specific option present in an ::OptionSet, ::RadioSet, or ::CheckboxSet.
# Note that Options should *not* be added directly to a ValidForm.
# Instead, ::Container#add_fields should be used to add options to an option set, and that
# set should be added to the form.
##########################################################################################
class ValidForm::Option < ValidForm::Field

	# Indicates an ::OptionSet that this option is associated with.
	# (This field is set automatically when ::Container#add_fields is called)
	attr_accessor(:optionSet)

	# A boolean value indicating whether or not this option is chosen.
	attr_accessor(:chosen)

	# _id_::          	  The unique identifier for this option. [reqd]
	# _value_::       	  The initial String value for the option. [reqd]
	# _label_::       	  The human-friendly label for the option.
	# _attributes_:: 	  A Hash of various custom attributes to be associated with the option.
	# Passing the Symbol/value pair <tt>:chosen=>true</tt> in the _attributes_ collection will set the +chosen+ attribute for this option (and will then remove it from the _attributes_ Hash).
	#
	# See ::OptionSet#new for an example creating an ::Option.
	def initialize(id,value=nil,label=nil,attributes={})
		super(id,nil,value,label,attributes,{})
		self.chosen= @attributes.delete(:chosen)
	end

	def parent_container=(c) #:nodoc:
		raise ArgumentError,"ValidForm::Option objects may only be placed in a ValidForm::OptionSet" unless c.is_a?(ValidForm::OptionSet)
		@optionSet=c
	end

	def chosen=(c) #:nodoc:
		@optionSet.devalidate if @optionSet && !!c != @chosen
		@chosen = !!c
		@optionSet.unchoose_options_except(self) if c && @optionSet && !@optionSet.chooseMultiple
	end

	undef_method(:validate)
end



##########################################################################################
# Represents a collection of related options. These may be mutually exclusive, (e.g. a set
# of radio buttons, or a pull-down menu) or multiple may be chosen at once (e.g. a set of
# checkboxes, or a list of selectable options).
#
# Each option set keeps track of one or more ::Option objects, added through ::Container#add_fields.
#
# If the +value+ is set for the option set when it is created, any options passed to
# +add_fields+ with those values will be chosen automatically. This feature only occurs
# the first time +add_fields+ is called.
#
# See also ::Option#new for information on pre-choosing an option.
#
# See also ::RadioSet and ::CheckboxSet, subclasses used to specify the desired
# interface rendering.
##########################################################################################
class ValidForm::OptionSet < ValidForm::Field
	include ValidForm::Container

	# Truth value indicating if a value other than those available through the options may be entered; +false+ by default. <b>Not yet supported.</b>
	attr_accessor(:allowCustom)

	# Truth value indicating if the user may choose more than one option in the set; +false+ by default.
	attr_accessor(:chooseMultiple)

	# Array of all options currently tracked by the set. <i>Direct modifications to this array may cause unexpected errors. Use ::Container#add_fields to add new options to the set.</i>
	attr_reader(:options)

	# <i>(See ::Field#new for information on the parameters.)</i>
	#
	# Option sets are added to the form as a single field:
	#   search = ValidForm.new('search.rhtml','GET')
	#   search.add_fields(
	#   	ValidForm::Text.new('q','q',nil,'Find:'),
	#   	ValidForm::OptionSet.new('sections',nil,nil,'Look In:').add_fields(
	#   		ValidForm::Option.new('s1','slashdot.org','/.'),
	#   		ValidForm::Option.new('s2','google.com','Google'),
	#   		ValidForm::Option.new('s3','apple.com','Apple')
	#   	)
	#   )
	#   search.add_fields( ValidForm::Submit.new('fsubmit',nil,nil,'Go') )
	def initialize(*params)
		VALIDATION_KEYS.merge!({:minchosen=>1,:maxchosen=>1})
		super
		VALIDATION_KEYS.delete(:minchosen); VALIDATION_KEYS.delete(:maxchosen)
		@chooseMultiple = false
		@allowCustom = false
		allowed_container_classes(ValidForm::Option)
		#ToDo: allow FieldSet for <optgroup>s in HTML
	end

	
	alias_method :real_add_fields,:add_fields
	protected :real_add_fields
	def add_fields( *fields )  #:nodoc:
		return unless fields
		vals = values.compact
		self.real_add_fields( *fields )
		fields.each{ |f|
			f.chosen=true if vals.include?(f.value)
		}
		self
	end


	# Unchooses every option in the set (except for an optional _optionToSkip_ option which may be supplied).
	#
	# <i>(Primarily for internal use by the library to enforce a unique choice when +chooseMultiple+ is +false+.)</i>
	#
	# Note that this does not *choose* _optionToSkip_ (if supplied); that option's +chosen+ state is left unaffected.
	def unchoose_options_except(optionToSkip=nil)
		devalidate
		@fields.each{ |o| o.chosen=false unless o==optionToSkip }
	end


	# Returns an Array of the value of every option in the set that is chosen.
	#
	# If no option in the set is chosen, a zero-length Array is returned; if only one option is chosen, a single-element Array is returned. See also #value.
	def values
		if @fields
			vals=[]
			@fields.each{ |o| vals << o.value if o.chosen }
			vals
		else
			@value.is_a?(Array) ? @value : @value.respond_to?(:split) ? @value.split(',') : [@value]
		end
		
	end

	# If only one option is chosen, returns the value of that option. If more than one option is chosen, returns an Array of values. If no options are chosen, returns +nil+. See also #values.
	def value
		(vals=values).length==0 ? nil : vals.length==1 ? vals[0] : vals
		#ToDo: allow for @customValue
	end


	# _optionValue_:: May be a single value, a comma-delimited String of values, or an Array of values.
	# Chooses the option(s) whose value is in _optionValue_; unchooses all other options in the set.
	def value=( optionValue )
		#ToDo: allow for @customValue
		devalidate
		optionValue=optionValue.split(',') if optionValue.respond_to?(:split)
		optionValue=[optionValue] unless optionValue.is_a? Array
		optionValue.compact!
		@fields.each{ |o|
			val = o.value!=nil ? o.value.to_s : o.id.to_s
			o.chosen = optionValue.include?(val)
		}
	end


	# In addition to the validation rules listed in ::Field#validate, option sets have two additional rules which may be set:
	# +minchosen+::  An integer representing the minimum number of options which must be chosen to be valid.
	# +maxchosen+::  An integer representing the maximum number of options which may be chosen to be valid.
	# To ensure that (at least) one option is chosen for the set, either the +required+ or +minchosen+ validation rule may be used.
	def validate
		super
		@errors = [] unless @errors
		rs = @validationRules
		if rs[:minchosen] || rs[:maxchosen]
			numChosen = values.length
			@errors << ValidForm::ValidationError.new(self,ValidForm::ValidationError::TOO_FEW_CHOSEN)  if rs[:minchosen] && numChosen<rs[:minchosen]
			@errors << ValidForm::ValidationError.new(self,ValidForm::ValidationError::TOO_MANY_CHOSEN) if rs[:maxchosen] && numChosen>rs[:maxchosen]
		end

		@errors=nil if @errors.length==0
		@errors || true
	end
end


##########################################################################################
# A subclass of ::OptionSet used to indicate that the options should be drawn as radio buttons.
#
# +chooseMultiple+ and +allowCustom+ are both +false+ and may not be changed.
#
# See ::OptionSet#new for information on creating a RadioSet.
##########################################################################################
class ValidForm::RadioSet < ValidForm::OptionSet
	def initialize(*params) #:nodoc:
		super
		@chooseMultiple=false
		@allowCustom=false
	end
	undef_method(:chooseMultiple=,:allowCustom=)
end


##########################################################################################
# A subclass of ::OptionSet used to indicate that the options should be drawn as checkboxes.
#
# +chooseMultiple+ is +true+ and +allowCustom+ is +false+; neither may not be changed.
#
# See ::OptionSet#new for information on creating a CheckboxSet.
##########################################################################################
class ValidForm::CheckboxSet < ValidForm::OptionSet
	def initialize(*params) #:nodoc:
		super
		@chooseMultiple=true
		@allowCustom=false
	end
	undef_method(:chooseMultiple=,:allowCustom=)
end







##########################################################################################
# Describes a single validation error that occurred with a field.
# Each field may have more than one ::ValidationError after a call to ::Field#validate;
# see ::Field#errors for more information.
##########################################################################################
class ValidForm::ValidationError
	REQUIRED_VALUE_EMPTY = :REQUIRED_VALUE_EMPTY
	LENGTH_TOO_SMALL     = :LENGTH_TOO_SMALL
	LENGTH_TOO_LARGE     = :LENGTH_TOO_LARGE
	MATCH_FAIL_PHONE     = :MATCH_FAIL_PHONE
	MATCH_FAIL_EMAIL     = :MATCH_FAIL_EMAIL
	MATCH_FAIL_ZIPCODE   = :MATCH_FAIL_ZIPCODE
	MATCH_FAIL_INTEGER   = :MATCH_FAIL_INTEGER
	MATCH_FAIL_FLOAT     = :MATCH_FAIL_FLOAT
	MATCH_FAIL_DATE      = :MATCH_FAIL_DATE
	MATCH_FAIL_TIME      = :MATCH_FAIL_TIME
	MATCH_FAIL_DATETIME  = :MATCH_FAIL_DATETIME
	MATCH_FAIL_CUSTOM    = :MATCH_FAIL_CUSTOM
	VALUE_TOO_SMALL      = :VALUE_TOO_SMALL
	VALUE_TOO_LARGE      = :VALUE_TOO_LARGE
	DATETIME_TOO_SMALL   = :DATETIME_TOO_SMALL
	DATETIME_TOO_LARGE   = :DATETIME_TOO_LARGE
	TOO_FEW_CHOSEN       = :TOO_FEW_CHOSEN
	TOO_MANY_CHOSEN      = :TOO_MANY_CHOSEN

	# The field this error is associated with.
	attr_reader(:field)

	# One of the error constants of this class.
	attr_reader(:type)

	# The value of the field when the error occurred.
	attr_reader(:value)

	# A message that describes the error.
	attr_reader(:msg)

	# _field_:: A reference to the field that is invalid.  [reqd]
	# _errorType_::  One of the error constants associated with this class.  [reqd]
	def initialize(field,errorType)
		rs = field.validationRules
		case errorType
			when LENGTH_TOO_SMALL     then @msg = "%label% must be at least #{rs[:minlength]} characters. (It is only #{field.value.length}.)"
			when LENGTH_TOO_LARGE     then @msg = "%label% may not be more than #{rs[:maxlength]} characters. (It is currently #{field.value.length}.)"
			when VALUE_TOO_SMALL      then @msg = "%label% can't be less than #{rs[:minvalue]}."
			when VALUE_TOO_LARGE      then @msg = "%label% can't be more than #{rs[:maxvalue]}."
			when DATETIME_TOO_SMALL
				case rs[:type]
					when :date        then @msg = "%label% can't be before #{rs[:minvalue].custom_format('#M#/#D#/#YYYY#')}."
					when :time        then @msg = "%label% can't be before #{rs[:minvalue].custom_format('#h#:#mm#:#ss##ampm#')}."
					when :datetime    then @msg = "%label% can't be before #{rs[:minvalue].custom_format('#M#/#D#/#YYYY# #h#:#mm#:#ss##ampm#')}."
				end
			when DATETIME_TOO_LARGE
				case rs[:type]
					when :date     	  then @msg = "%label% can't be after #{rs[:maxvalue].custom_format('#M#/#D#/#YYYY#')}."
					when :time     	  then @msg = "%label% can't be after #{rs[:maxvalue].custom_format('#h#:#mm#:#ss##ampm#')}."
					when :datetime    then @msg = "%label% can't be after #{rs[:maxvalue].custom_format('#M#/#D#/#YYYY# #h#:#mm#:#ss##ampm#')}."
				end

			when TOO_FEW_CHOSEN       then @msg = "Please choose at least #{rs[:minchosen]} option#{rs[:minchosen]!=1 ? 's' : ''} for %label%."
			when TOO_MANY_CHOSEN      then @msg = "Please choose no more than #{rs[:maxchosen]} option#{rs[:maxchosen]!=1 ? 's' : ''} for %label%."

			when REQUIRED_VALUE_EMPTY then @msg = rs[:reqfail_msg]  || "%label% is a required field."
			when MATCH_FAIL_PHONE     then @msg = rs[:typefail_msg] || "%label% does not look like a valid phone number.\nIt should be like: (123) 456-7890 or 123.456.7890"
			when MATCH_FAIL_EMAIL     then @msg = rs[:typefail_msg] || "%label% does not look like a valid email address.\nIt should be like: john@somehost.com"
			when MATCH_FAIL_ZIPCODE   then @msg = rs[:typefail_msg] || "%label% does not look like a valid zip code.\nIt should be like: 19009 or 19009-0723"
			when MATCH_FAIL_INTEGER   then @msg = rs[:typefail_msg] || "%label% must be an integer."
			when MATCH_FAIL_FLOAT     then @msg = rs[:typefail_msg] || "%label% must be a number, such as 1024 or 3.1415 (no commas are allowed)."
			when MATCH_FAIL_DATE      then @msg = rs[:typefail_msg] || "%label% does not look like a valid date.\nIt should be like: 12/31/2004"
			when MATCH_FAIL_TIME      then @msg = rs[:typefail_msg] || "%label% does not look like a valid time.\nIt should be like: 11pm or 1:13:57 or 23:00"
			when MATCH_FAIL_DATETIME  then @msg = rs[:typefail_msg] || "%label% does not look like a valid date/time.\nIt should be like: 12/31/2004 1:13pm"
			when MATCH_FAIL_CUSTOM    then @msg = rs[:typefail_msg] || "%label% does not look appear to be in a valid format."
			else
				raise ArgumentError,"Unrecognized error type passed to ValidForm::ValidationError#new"
		end
		@msg.gsub!(%r|%label%|, field.label || field.name || field.id )
		@type=type
		@field=field
		@value=field.value
	end
end

