class FormInput
Class for easy form input processing and management.
Support for multi-step forms.
Extend forms with arguments for form parameter types which are used often.
Version
number.
Constants
- BOOL_ARGS
Boolean value, displayed as a select menu.
- CHECKBOX_ARGS
Boolean value, displayed as a checkbox.
- DEFAULT_ENCODING
Encoding we convert all input request parameters into.
- DEFAULT_ERROR_MESSAGES
Hash
mapping error codes to default error messages.- DEFAULT_FILTER
Default input filter applied to all input.
- DEFAULT_MAX_KEY
Maximum hash key value we allow by default.
- DEFAULT_MIN_KEY
Minimum hash key value we allow by default.
- DEFAULT_SIZE_LIMIT
Default size limit applied to all input.
- EMAIL_ARGS
Email.
- EU_DATE_ARGS
Time in EU date format.
- EU_DATE_FORMAT
EU date format.
- EU_DATE_FORMAT_EXAMPLE
EU date format example.
- FLOAT_ARGS
Float number.
- HOURS_ARGS
Seconds since midnight in hours:minutes format.
- HOURS_FORMAT
Hours format.
- HOURS_FORMAT_EXAMPLE
Hours format example.
- INTEGER_ARGS
Integer number.
- LATIN_NAMES_RE
Matches names using latin alphabet.
- MERGEABLE_OPTIONS
Parameter
options which can be merged together into an array when multiple option hashes are merged.- PHONE_ARGS
Phone number.
- PHONE_NUMBER_FILTER
Filter for phone numbers.
- PHONE_NUMBER_RE
Matches generic phone number.
- PRUNED_ARGS
Transformation which drops empty values from hashes and arrays and turns empty string into nil.
- SIMPLE_EMAIL_RE
Matches common email addresses. Note that it doesn't match all addresses allowed by RFC, though.
- TIME_ARGS
Full time.
- TIME_FORMAT
Full time format.
- TIME_FORMAT_EXAMPLE
Full time format example.
- UK_DATE_ARGS
Time in UK date format.
- UK_DATE_FORMAT
UK date format.
- UK_DATE_FORMAT_EXAMPLE
UK date format example.
- US_DATE_ARGS
Time in US date format.
- US_DATE_FORMAT
US date format.
- US_DATE_FORMAT_EXAMPLE
US date format example.
- ZIP_ARGS
Zip code.
- ZIP_CODE_RE
Matches generic ZIP code. Note that the real format changes for each country.
Public Class Methods
Get given parameter(s), hash style.
# File lib/form_input/core.rb, line 629 def []( *names ) if names.count == 1 form_params[ names.first ] else form_params.values_at( *names ) end end
Like param, except that it defines array parameter.
# File lib/form_input/core.rb, line 727 def array( name, *args, &block ) param( name, *args, array: true, &block ) end
Like param!, except that it defines required array parameter.
# File lib/form_input/core.rb, line 732 def array!( name, *args, &block ) param!( name, *args, array: true, &block ) end
Copy given/all form parameters. Returns self for chaining.
# File lib/form_input/core.rb, line 652 def copy( source, opts = {} ) case source when Parameter add( Parameter.new( opts[ :name ] || source.name, opts[ :code ] || opts[ :name ] || source.code, source.opts.merge( opts ) ) ) when Array source.each{ |x| copy( x, opts ) } when Class fail ArgumentError, "invalid source form #{source.inspect}" unless source < FormInput copy( source.form_params.values, opts ) else fail ArgumentError, "invalid source parameter #{source.inspect}" end self end
Get string containing YAML representation of the default R18n translation for all/given FormInput
classes.
# File lib/form_input/localize.rb, line 43 def self.default_translation( forms = self.forms ) hash = Hash[ forms.map{ |x| [ x.translation_name, x.translation_hash ] }.reject{ |k, v| v.empty? } ] YAML::dump( { forms: hash }.stringify_keys ) end
Turn this form into multi-step form using given steps. Returns self for chaining.
# File lib/form_input/steps.rb, line 7 def self.define_steps( steps ) @steps = steps = steps.to_hash.dup.freeze self.send( :include, StepMethods ) opts = { filter: ->{ steps.keys.find{ |x| x.to_s == self } }, class: Symbol } param :step, opts, type: :hidden param :next, opts, type: :ignore param :last, opts, type: :hidden param :seen, opts, type: :hidden self end
Iterate over each possible inflection for given inflection string and return first non-nil result. You may override this if you need more complex inflection fallbacks for some locale.
# File lib/form_input/r18n.rb, line 129 def self.find_inflection( string ) until string.empty? break if result = yield( string ) string = string[0..-2] end result end
Get hash mapping parameter names to parameters themselves.
# File lib/form_input/core.rb, line 624 def form_params @params ||= {} end
Get hash mapping defined steps to their names, or nil if there are none.
# File lib/form_input/steps.rb, line 23 def self.form_steps @steps end
Get list of all classes inherited from FormInput
.
# File lib/form_input/localize.rb, line 38 def self.forms ObjectSpace.each_object( Class ).select{ |x| x < FormInput and x.name }.sort_by{ |x| x.name } end
Create new form from hash with internal values.
# File lib/form_input/core.rb, line 758 def from_hash( hash ) new.set( hash ) end
Create new form from hash with external values.
# File lib/form_input/core.rb, line 752 def from_params( params ) new.import( params ) end
Create new form from request with external values.
# File lib/form_input/core.rb, line 747 def from_request( request ) new.import( request ) end
Like param, except that it defines hash parameter.
# File lib/form_input/core.rb, line 737 def hash( name, *args, &block ) param( name, *args, hash: true, &block ) end
Like param!, except that it defines required hash parameter.
# File lib/form_input/core.rb, line 742 def hash!( name, *args, &block ) param!( name, *args, hash: true, &block ) end
Create standalone copy of form parameters in case someone inherits an existing form.
# File lib/form_input/core.rb, line 619 def inherited( into ) into.instance_variable_set( '@params', form_params.dup ) end
Create new form info, initializing it from given hash or request, if anything.
# File lib/form_input/core.rb, line 775 def initialize( *args ) @params = bound_params @errors = nil for arg in args if arg.is_a? Hash set( arg ) else import( arg ) end end end
Define form parameter with given name, code, title, maximum size, options, and filter block. All fields except name are optional. In case the code is missing, name is used instead. If no size limits are specified, 255 characters and bytes limits are applied by default. If no filter is explicitly defined, default filter squeezing and stripping whitespace is applied. Returns self for chaining.
# File lib/form_input/core.rb, line 676 def param( name, *args, &block ) # Fetch arguments. code = name code = args.shift if args.first.is_a? Symbol title = args.shift if args.first.is_a? String size = args.shift if args.first.is_a? Numeric opts = {} opts.merge!( args.shift ){ |k, o, n| ( n && MERGEABLE_OPTIONS.include?( k ) ) ? [ *o, *n ] : n } while args.first.is_a? Hash fail ArgumentError, "invalid arguments #{args}" unless args.empty? # Set the title. opts[ :title ] = title.freeze if title # Set input filter. opts[ :filter ] = block if block opts[ :filter ] = DEFAULT_FILTER unless opts.key?( :filter ) # Enforce default size limits for any input processed. limit = DEFAULT_SIZE_LIMIT size = ( opts[ :max_size ] ||= size || limit ) opts[ :max_bytesize ] ||= limit if size.is_a?( Proc ) or size <= limit # Set default key limits for hash parameters. if opts[ :hash ] opts[ :min_key ] ||= DEFAULT_MIN_KEY opts[ :max_key ] ||= DEFAULT_MAX_KEY end # Define parameter. add( Parameter.new( name, code, opts ) ) self end
Like param, except this defines required parameter.
# File lib/form_input/core.rb, line 722 def param!( name, *args, &block ) param( name, *args, required: true, &block ) end
Parse time like Time#strptime but raise on trailing garbage. Also ignores -, _ and ^ % modifiers, so the same format can be used for both parsing and formatting.
# File lib/form_input/types.rb, line 143 def self.parse_time( string, format ) format = format.gsub( /%[-_^]?(.)/, '%\1' ) # Rather than using _strptime and checking the leftover field, # add required trailing character to both the string and format parameters. suffix = ( string[ -1 ] == "\1" ? "\2" : "\1" ) Time.strptime( "+0000 #{string}#{suffix}", "%z #{format}#{suffix}" ).utc end
Like parse_time
, but falls back to DateTime.parse heuristics when the date/time can't be parsed.
# File lib/form_input/types.rb, line 152 def self.parse_time!( string, format ) parse_time( string, format ) rescue DateTime.parse( string ).to_time.utc end
Get hash of all form values which may need to be localized.
# File lib/form_input/localize.rb, line 31 def self.translation_hash hash = Hash[ form_params.map{ |k, v| [ k, v.translation_hash ] }.reject{ |k, v| v.empty? } ] hash[ :steps ] = form_steps.reject{ |k, v| v.nil? } if form_steps hash end
Get name of the form used as translation scope for text translations.
# File lib/form_input/r18n.rb, line 112 def self.translation_name @translation_name ||= name.split( '::' ).last .gsub( /([A-Z]+)([A-Z][a-z])/, '\1_\2' ) .gsub( /([a-z\d])([A-Z])/, '\1_\2' ) .downcase end
Get path to R18n translations provided by this gem.
# File lib/form_input/r18n.rb, line 107 def self.translations_path File.expand_path( "#{__FILE__}/../r18n" ) end
Private Class Methods
Add given parameter to the form, after performing basic validity checks.
# File lib/form_input/core.rb, line 638 def add( param ) name = param.name fail ArgumentError, "duplicate parameter #{name}" if form_params[ name ] fail ArgumentError, "invalid parameter name #{name}" if method_defined?( name ) self.send( :attr_accessor, name ) form_params[ name ] = param end
Public Instance Methods
Get given parameter(s) value(s), hash style.
# File lib/form_input/core.rb, line 890 def []( *names ) if names.count == 1 send( names.first ) else names.map{ |x| send( x ) } end end
Set given parameter value, hash style. Unlike setting the attribute directly, this triggers a revalidation in the future.
# File lib/form_input/core.rb, line 900 def []=( name, value ) @errors = nil send( "#{name}=", value ) end
Get list of array parameters.
# File lib/form_input/core.rb, line 1060 def array_params params.select{ |x| x.array? } end
Get list of parameters with blank values.
# File lib/form_input/core.rb, line 988 def blank_params params.select{ |x| x.blank? } end
Build URL from given URL and combination of current paramaters and provided parameters.
# File lib/form_input/core.rb, line 1138 def build_url( url, args = {} ) dup.set( args ).extend_url( url ) end
Get all/given parameters chunked into individual rows for nicer form display.
# File lib/form_input/core.rb, line 1102 def chunked_params( params = self.params ) params.chunk{ |p| p[ :row ] || :_alone }.map{ |x,a| a.count > 1 ? a : a.first } end
Clear all/given parameter values. Both names and parameters are accepted. Returns self for chaining.
# File lib/form_input/core.rb, line 879 def clear( *names ) names = names.empty? ? params_names : validate_names( names ) for name in names # Set the value to nil first so it triggers anything necessary. self[ name ] = nil remove_instance_variable( "@#{name}" ) end self end
Get list of parameters with correct value types.
# File lib/form_input/core.rb, line 976 def correct_params params.select{ |x| x.correct? } end
Get list of disabled parameters.
# File lib/form_input/core.rb, line 1030 def disabled_params params.select{ |x| x.disabled? } end
Return true if all parameters are empty.
# File lib/form_input/core.rb, line 1109 def empty? filled_params.empty? end
Get list of parameters with empty values.
# File lib/form_input/core.rb, line 994 def empty_params params.select{ |x| x.empty? } end
Get list of enabled parameters.
# File lib/form_input/core.rb, line 1036 def enabled_params params.select{ |x| x.enabled? } end
Get first error for given parameter. Returns nil if there were no errors.
# File lib/form_input/core.rb, line 1179 def error_for( name ) errors_for( name ).first end
Get list of error messages, but including only the first one reported for each parameter.
# File lib/form_input/core.rb, line 1151 def error_messages errors.values.map{ |x| x.first } end
Get hash of all errors detected for each parameter.
# File lib/form_input/core.rb, line 1145 def errors validate? @errors.dup end
Get list of errors for given parameter. Returns empty list if there were no errors.
# File lib/form_input/core.rb, line 1174 def errors_for( name ) errors[ name ] || [] end
Create copy of itself, with given parameters unset. Both names and parameters are accepted.
# File lib/form_input/core.rb, line 936 def except( *names ) dup.clear( names ) end
Extend given URL with query created from all current non-empty parameters.
# File lib/form_input/core.rb, line 1128 def extend_url( url ) url = url.to_s.dup query = url_query unless query.empty? url << ( url['?'] ? '&' : '?' ) << query end url end
Get list of parameters with non-empty values.
# File lib/form_input/core.rb, line 1000 def filled_params params.select{ |x| x.filled? } end
Freeze the form.
# File lib/form_input/core.rb, line 802 def freeze unless frozen? validate? @errors.freeze.each{ |k,v| v.freeze } end super end
Like t helper, except that the translation is looked up in the forms.<form_name> scope. Supports both ft.name( args ) and ft( :name, args ) forms.
# File lib/form_input/r18n.rb, line 121 def ft( *args ) fail "You need to set the locale with R18n.set('en') or similar. No locale, no helper. Sorry." unless r18n translation = t.forms[ self.class.translation_name ] args.empty? ? translation : translation[ *args ] end
Get list of hash parameters.
# File lib/form_input/core.rb, line 1066 def hash_params params.select{ |x| x.hash? } end
Get list of ignored parameters.
# File lib/form_input/core.rb, line 1048 def ignored_params params.select{ |x| x.ignored? } end
Import parameter values from given request or hash. Applies parameter input filters and transforms as well. Returns self for chaining.
# File lib/form_input/core.rb, line 847 def import( request ) hash = request.respond_to?( :params ) ? request.params : request.to_hash for name, param in @params value = hash.fetch( param.code ) { hash.fetch( param.code.to_s, self ) } unless value == self value = sanitize_value( value, param.filter ) if transform = param.transform value = value.instance_exec( &transform ) end self[ name ] = value end end self end
Get list of parameters with incorrect value types.
# File lib/form_input/core.rb, line 982 def incorrect_params params.select{ |x| x.incorrect? } end
Initialize form clone.
# File lib/form_input/core.rb, line 788 def initialize_clone( other ) super @params = bound_params @errors &&= Hash[ @errors.map{ |k,v| [ k, v.clone ] } ] end
Initialize form copy.
# File lib/form_input/core.rb, line 795 def initialize_dup( other ) super @params = bound_params @errors = nil end
Test if there were some errors (overall or for given parameters) reported.
# File lib/form_input/core.rb, line 1193 def invalid?( *names ) not valid?( *names ) end
Get list of parameters with some errors reported.
# File lib/form_input/core.rb, line 1096 def invalid_params params.select{ |x| x.invalid? } end
Get list of given named parameters. Note that nil is returned for unknown names, and duplicate parameters for duplicate names.
# File lib/form_input/core.rb, line 970 def named_params( *names ) @params.values_at( *names ) end
Create copy of itself, with only given parameters set. Both names and parameters are accepted.
# File lib/form_input/core.rb, line 941 def only( *names ) # It would be easier to create new instance here and only copy selected values, # but we want to use dup instead of new here, as the derived form can use # different parameters in its construction. dup.clear( params_names - validate_names( names ) ) end
Get list of optional parameters.
# File lib/form_input/core.rb, line 1024 def optional_params params.select{ |x| x.optional? } end
Get given named parameter.
# File lib/form_input/core.rb, line 951 def param( name ) @params[ name ] end
Get list of all parameters.
# File lib/form_input/core.rb, line 957 def params @params.values end
Get list of all parameter names.
# File lib/form_input/core.rb, line 963 def params_names @params.keys end
Remember error concerning given parameter. In case of multiple errors, the message is added to the end of the list, making it less important than the other errors. Returns self for chaining.
# File lib/form_input/core.rb, line 1158 def report( name, msg ) validate? ( @errors[ name ] ||= [] ) << msg.to_s.dup.freeze self end
Remember error concerning given parameter. In case of multiple errors, the message is added to the beginning of the list, making it more important than the other errors. Returns self for chaining.
# File lib/form_input/core.rb, line 1167 def report!( name, msg ) validate? ( @errors[ name ] ||= [] ).unshift( msg.to_s.dup.freeze ) self end
Get list of required parameters.
# File lib/form_input/core.rb, line 1018 def required_params params.select{ |x| x.required? } end
Get list of scalar parameters.
# File lib/form_input/core.rb, line 1072 def scalar_params params.select{ |x| x.scalar? } end
Set parameter values from given hash. Returns self for chaining.
# File lib/form_input/core.rb, line 864 def set( hash ) for name, value in hash self[ name ] = value end self end
Get list of parameters whose values were set.
# File lib/form_input/core.rb, line 1006 def set_params params.select{ |x| x.set? } end
Get list of parameters tagged with given/any tags.
# File lib/form_input/core.rb, line 1078 def tagged_params( *tags ) params.select{ |x| x.tagged?( *tags ) } end
Return all set parameters as a hash. Note that the keys are external names of the parameters (should they differ), so the keys created by `from_data(data).to_data` remain consistent. See also to_hash
, which creates a hash of non-empty parameters.
# File lib/form_input/core.rb, line 919 def to_data result = {} set_params.each{ |x| result[ x.code ] = x.value } result end
Return all non-empty parameters as a hash. See also to_data
, which creates a hash of set parameters, and url_params
, which creates a hash suitable for url output.
# File lib/form_input/core.rb, line 908 def to_hash result = {} filled_params.each{ |x| result[ x.name ] = x.value } result end
Unset values of given parameters. Both names and parameters are accepted. Returns self for chaining.
# File lib/form_input/core.rb, line 873 def unset( name, *names ) clear( name, *names ) end
Get list of parameters whose values were not set.
# File lib/form_input/core.rb, line 1012 def unset_params params.select{ |x| x.unset? } end
Get list of parameters not tagged with given/any tags.
# File lib/form_input/core.rb, line 1084 def untagged_params( *tags ) params.select{ |x| x.untagged?( *tags ) } end
Get hash of all non-empty parameters for use in URL.
# File lib/form_input/core.rb, line 1114 def url_params result = {} filled_params.each{ |x| result[ x.code ] = x.form_value } result end
Create string containing URL query from all current non-empty parameters.
# File lib/form_input/core.rb, line 1123 def url_query Rack::Utils.build_nested_query( url_params ) end
Return parameter(s) value(s) as long as they are all valid, nil otherwise.
# File lib/form_input/core.rb, line 1198 def valid( name, *names ) self[ name, *names ] if valid?( name, *names ) end
Test if there were no errors (overall or for given parameters) reported.
# File lib/form_input/core.rb, line 1184 def valid?( *names ) if names.empty? errors.empty? else validate_names( names ).all?{ |x| errors_for( x ).empty? } end end
Get list of parameters with no errors reported.
# File lib/form_input/core.rb, line 1090 def valid_params params.select{ |x| x.valid? } end
Validate parameter values and remember any errors detected. You can override this in your class if you need more specific validation and :check callback is not good enough. Just make sure to call super first. Returns self for chaining.
# File lib/form_input/core.rb, line 1206 def validate @errors ||= {} params.each{ |x| x.validate } self end
Like validate, except that it forces revalidation of all parameters. Returns self for chaining.
# File lib/form_input/core.rb, line 1214 def validate! @errors = {} validate self end
Like validate, except that it does nothing if validation was already done. Returns self for chaining.
# File lib/form_input/core.rb, line 1222 def validate? validate unless @errors self end
Get list of visible parameters.
# File lib/form_input/core.rb, line 1054 def visible_params params.select{ |x| x.visible? } end
Private Instance Methods
Get copy of parameter hash with parameters bound to this form.
# File lib/form_input/core.rb, line 767 def bound_params hash = {} self.class.form_params.each{ |name, param| hash[ name ] = param.dup.bind( self ) } hash.freeze end
Import request parameter value.
# File lib/form_input/core.rb, line 811 def sanitize_value( value, filter = nil ) case value when String # Note that Rack does no encoding processing as of now, # and even if we know content type charset, the query charset # is not well defined and we can't fix the multi-part input here either. # So we just hope that all clients will send the data in UTF-8 which we used in the form, # and enforce everything to UTF-8. If it is not valid, we keep the binary string instead # so the validation can detect it but the user can still process it himself if he wants to. value = value.dup.force_encoding( DEFAULT_ENCODING ) if value.valid_encoding? value = value.instance_exec( &filter ) if filter else value.force_encoding( Encoding::BINARY ) end value when Array # Arrays are supported, but note that the validation done later only allows flat arrays. value.map{ |x| sanitize_value( x, filter ) } when Hash # To reduce security issues, we prefer integer hash keys only. # The validation done later ensures that the keys are valid, within range, # and that only flat hashes are allowed. Hash[ value.map{ |k, v| [ ( Integer( k, 10 ) rescue k ), sanitize_value( v, filter ) ] } ] when Numeric, TrueClass, FalseClass, NilClass # For convenience of importing JSON payloads, allow each of these simple scalar types as they are. # The validation done later will ensure that the type class matches the parameter. value else fail TypeError, "unexpected parameter type" end end
Convert parameters to names and fail if we encounter unknown one.
# File lib/form_input/core.rb, line 926 def validate_names( names ) names.flatten.map do |name| name = name.name if name.is_a? Parameter fail( ArgumentError, "unknown parameter #{name}" ) unless @params[ name ] name end end