szilm-validates_timeliness 2.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. data/CHANGELOG +116 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +403 -0
  4. data/Rakefile +52 -0
  5. data/TODO +8 -0
  6. data/lib/validates_timeliness.rb +49 -0
  7. data/lib/validates_timeliness/action_view/instance_tag.rb +52 -0
  8. data/lib/validates_timeliness/active_record/attribute_methods.rb +77 -0
  9. data/lib/validates_timeliness/active_record/multiparameter_attributes.rb +69 -0
  10. data/lib/validates_timeliness/formats.rb +365 -0
  11. data/lib/validates_timeliness/locale/en.yml +18 -0
  12. data/lib/validates_timeliness/matcher.rb +1 -0
  13. data/lib/validates_timeliness/parser.rb +44 -0
  14. data/lib/validates_timeliness/spec/rails/matchers/validate_timeliness.rb +162 -0
  15. data/lib/validates_timeliness/validation_methods.rb +46 -0
  16. data/lib/validates_timeliness/validator.rb +230 -0
  17. data/lib/validates_timeliness/version.rb +3 -0
  18. data/spec/action_view/instance_tag_spec.rb +194 -0
  19. data/spec/active_record/attribute_methods_spec.rb +157 -0
  20. data/spec/active_record/multiparameter_attributes_spec.rb +118 -0
  21. data/spec/formats_spec.rb +306 -0
  22. data/spec/ginger_scenarios.rb +19 -0
  23. data/spec/parser_spec.rb +61 -0
  24. data/spec/resources/application.rb +2 -0
  25. data/spec/resources/person.rb +3 -0
  26. data/spec/resources/schema.rb +10 -0
  27. data/spec/resources/sqlite_patch.rb +19 -0
  28. data/spec/spec/rails/matchers/validate_timeliness_spec.rb +245 -0
  29. data/spec/spec_helper.rb +58 -0
  30. data/spec/time_travel/MIT-LICENSE +20 -0
  31. data/spec/time_travel/time_extensions.rb +33 -0
  32. data/spec/time_travel/time_travel.rb +12 -0
  33. data/spec/validator_spec.rb +713 -0
  34. metadata +102 -0
@@ -0,0 +1,18 @@
1
+ en:
2
+ activerecord:
3
+ errors:
4
+ messages:
5
+ invalid_date: "is not a valid date"
6
+ invalid_time: "is not a valid time"
7
+ invalid_datetime: "is not a valid datetime"
8
+ is_at: "must be at {{restriction}}"
9
+ before: "must be before {{restriction}}"
10
+ on_or_before: "must be on or before {{restriction}}"
11
+ after: "must be after {{restriction}}"
12
+ on_or_after: "must be on or after {{restriction}}"
13
+ between: "must be between {{earliest}} and {{latest}}"
14
+ validates_timeliness:
15
+ error_value_formats:
16
+ date: '%Y-%m-%d'
17
+ time: '%H:%M:%S'
18
+ datetime: '%Y-%m-%d %H:%M:%S'
@@ -0,0 +1 @@
1
+ require 'validates_timeliness/spec/rails/matchers/validate_timeliness'
@@ -0,0 +1,44 @@
1
+ module ValidatesTimeliness
2
+ module Parser
3
+
4
+ class << self
5
+
6
+ def parse(raw_value, type, options={})
7
+ return nil if raw_value.blank?
8
+ return raw_value if raw_value.acts_like?(:time) || raw_value.is_a?(Date)
9
+
10
+ time_array = ValidatesTimeliness::Formats.parse(raw_value, type, options.reverse_merge(:strict => true))
11
+ return nil if time_array.nil?
12
+
13
+ if type == :date
14
+ Date.new(*time_array[0..2]) rescue nil
15
+ else
16
+ make_time(time_array[0..7])
17
+ end
18
+ end
19
+
20
+ def make_time(time_array)
21
+ # Enforce date part validity which Time class does not
22
+ return nil unless Date.valid_civil?(*time_array[0..2])
23
+
24
+ if Time.respond_to?(:zone) && ValidatesTimeliness.use_time_zones
25
+ Time.zone.local(*time_array)
26
+ else
27
+ # Older AR way of handling times with datetime fallback
28
+ begin
29
+ time_zone = ValidatesTimeliness.default_timezone
30
+ Time.send(time_zone, *time_array)
31
+ rescue ArgumentError, TypeError
32
+ zone_offset = time_zone == :local ? DateTime.local_offset : 0
33
+ time_array.pop # remove microseconds
34
+ DateTime.civil(*(time_array << zone_offset))
35
+ end
36
+ end
37
+ rescue ArgumentError, TypeError
38
+ nil
39
+ end
40
+
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,162 @@
1
+ module Spec
2
+ module Rails
3
+ module Matchers
4
+ class ValidateTimeliness
5
+
6
+ VALIDITY_TEST_VALUES = {
7
+ :date => {:pass => '2000-01-01', :fail => '2000-01-32'},
8
+ :time => {:pass => '12:00', :fail => '25:00'},
9
+ :datetime => {:pass => '2000-01-01 00:00:00', :fail => '2000-01-32 00:00:00'}
10
+ }
11
+
12
+ OPTION_TEST_SETTINGS = {
13
+ :is_at => { :method => :+, :modify_on => :invalid },
14
+ :before => { :method => :-, :modify_on => :valid },
15
+ :after => { :method => :+, :modify_on => :valid },
16
+ :on_or_before => { :method => :+, :modify_on => :invalid },
17
+ :on_or_after => { :method => :-, :modify_on => :invalid }
18
+ }
19
+
20
+ def initialize(attribute, options)
21
+ @expected, @options = attribute, options
22
+ @validator = ValidatesTimeliness::Validator.new(options)
23
+ end
24
+
25
+ def matches?(record)
26
+ @record = record
27
+ @type = @options[:type]
28
+
29
+ valid = test_validity
30
+
31
+ valid = test_option(:is_at) if valid && @options[:is_at]
32
+ valid = test_option(:before) if valid && @options[:before]
33
+ valid = test_option(:after) if valid && @options[:after]
34
+ valid = test_option(:on_or_before) if valid && @options[:on_or_before]
35
+ valid = test_option(:on_or_after) if valid && @options[:on_or_after]
36
+ valid = test_between if valid && @options[:between]
37
+
38
+ return valid
39
+ end
40
+
41
+ def failure_message
42
+ "expected model to validate #{@type} attribute #{@expected.inspect} with #{@last_failure}"
43
+ end
44
+
45
+ def negative_failure_message
46
+ "expected not to validate #{@type} attribute #{@expected.inspect}"
47
+ end
48
+
49
+ def description
50
+ "have validated #{@type} attribute #{@expected.inspect}"
51
+ end
52
+
53
+ private
54
+
55
+ def test_validity
56
+ invalid_value = VALIDITY_TEST_VALUES[@type][:fail]
57
+ valid_value = parse_and_cast(VALIDITY_TEST_VALUES[@type][:pass])
58
+ error_matching(invalid_value, "invalid_#{@type}".to_sym) &&
59
+ no_error_matching(valid_value, "invalid_#{@type}".to_sym)
60
+ end
61
+
62
+ def test_option(option)
63
+ settings = OPTION_TEST_SETTINGS[option]
64
+ boundary = parse_and_cast(@options[option])
65
+
66
+ method = settings[:method]
67
+
68
+ valid_value, invalid_value = if settings[:modify_on] == :valid
69
+ [ boundary.send(method, 1), boundary ]
70
+ else
71
+ [ boundary, boundary.send(method, 1) ]
72
+ end
73
+
74
+ error_matching(invalid_value, option) &&
75
+ no_error_matching(valid_value, option)
76
+ end
77
+
78
+ def test_before
79
+ before = parse_and_cast(@options[:before])
80
+
81
+ error_matching(before - 1, :before) &&
82
+ no_error_matching(before, :before)
83
+ end
84
+
85
+ def test_between
86
+ between = parse_and_cast(@options[:between])
87
+
88
+ error_matching(between.first - 1, :between) &&
89
+ error_matching(between.last + 1, :between) &&
90
+ no_error_matching(between.first, :between) &&
91
+ no_error_matching(between.last, :between)
92
+ end
93
+
94
+ def parse_and_cast(value)
95
+ value = @validator.class.send(:evaluate_option_value, value, @type, @record)
96
+ @validator.class.send(:type_cast_value, value, @type)
97
+ end
98
+
99
+ def error_matching(value, option)
100
+ match = error_message_for(option)
101
+ @record.send("#{@expected}=", value)
102
+ @record.valid?
103
+ errors = @record.errors.on(@expected)
104
+ pass = [ errors ].flatten.any? {|error| /#{match}/ === error }
105
+ @last_failure = "error matching '#{match}' when value is #{format_value(value)}" unless pass
106
+ pass
107
+ end
108
+
109
+ def no_error_matching(value, option)
110
+ pass = !error_matching(value, option)
111
+ unless pass
112
+ error = error_message_for(option)
113
+ @last_failure = "no error matching '#{error}' when value is #{format_value(value)}"
114
+ end
115
+ pass
116
+ end
117
+
118
+ def error_message_for(message)
119
+ restriction = @validator.class.send(:evaluate_option_value, @validator.configuration[message], @type, @record)
120
+
121
+ if restriction
122
+ restriction = @validator.class.send(:type_cast_value, restriction, @type)
123
+ interpolate = @validator.send(:interpolation_values, message, restriction)
124
+ end
125
+
126
+ if defined?(I18n)
127
+ interpolate ||= {}
128
+ options = interpolate.merge(:default => @validator.send(:custom_error_messages)[message])
129
+ if defined?(ActiveRecord::Error)
130
+ ActiveRecord::Error.new(@record, @expected, message, options).message
131
+ else
132
+ @record.errors.generate_message(@expected, message, options)
133
+ end
134
+ else
135
+ interpolate ||= nil
136
+ @validator.error_messages[message] % interpolate
137
+ end
138
+ end
139
+
140
+ def format_value(value)
141
+ return value if value.is_a?(String)
142
+ value.strftime(@validator.class.error_value_formats[@type])
143
+ end
144
+ end
145
+
146
+ def validate_date(attribute, options={})
147
+ options[:type] = :date
148
+ ValidateTimeliness.new(attribute, options)
149
+ end
150
+
151
+ def validate_time(attribute, options={})
152
+ options[:type] = :time
153
+ ValidateTimeliness.new(attribute, options)
154
+ end
155
+
156
+ def validate_datetime(attribute, options={})
157
+ options[:type] = :datetime
158
+ ValidateTimeliness.new(attribute, options)
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,46 @@
1
+ module ValidatesTimeliness
2
+ module ValidationMethods
3
+
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ module ClassMethods
9
+
10
+ def validates_time(*attr_names)
11
+ configuration = attr_names.extract_options!
12
+ configuration[:type] = :time
13
+ validates_timeliness_of(attr_names, configuration)
14
+ end
15
+
16
+ def validates_date(*attr_names)
17
+ configuration = attr_names.extract_options!
18
+ configuration[:type] = :date
19
+ validates_timeliness_of(attr_names, configuration)
20
+ end
21
+
22
+ def validates_datetime(*attr_names)
23
+ configuration = attr_names.extract_options!
24
+ configuration[:type] = :datetime
25
+ validates_timeliness_of(attr_names, configuration)
26
+ end
27
+
28
+ private
29
+
30
+ def validates_timeliness_of(attr_names, configuration)
31
+ validator = ValidatesTimeliness::Validator.new(configuration.symbolize_keys)
32
+
33
+ # bypass handling of allow_nil and allow_blank to validate raw value
34
+ configuration.delete(:allow_nil)
35
+ configuration.delete(:allow_blank)
36
+ validates_each(attr_names, configuration) do |record, attr_name, value|
37
+ validator.call(record, attr_name, value)
38
+ end
39
+ end
40
+
41
+ end
42
+
43
+ end
44
+ end
45
+
46
+ ActiveRecord::Base.send(:include, ValidatesTimeliness::ValidationMethods)
@@ -0,0 +1,230 @@
1
+ #TODO remove deprecated option :equal_to
2
+ module ValidatesTimeliness
3
+
4
+ class Validator
5
+ cattr_accessor :error_value_formats
6
+ cattr_accessor :ignore_restriction_errors
7
+ self.ignore_restriction_errors = false
8
+
9
+ RESTRICTION_METHODS = {
10
+ :is_at => :==,
11
+ :equal_to => :==,
12
+ :before => :<,
13
+ :after => :>,
14
+ :on_or_before => :<=,
15
+ :on_or_after => :>=,
16
+ :between => lambda {|v, r| (r.first..r.last).include?(v) }
17
+ }
18
+
19
+ VALID_OPTION_KEYS = [
20
+ :on, :if, :unless, :allow_nil, :empty, :allow_blank,
21
+ :with_time, :with_date, :ignore_usec, :format,
22
+ :invalid_time_message, :invalid_date_message, :invalid_datetime_message
23
+ ] + RESTRICTION_METHODS.keys.map {|option| [option, "#{option}_message".to_sym] }.flatten
24
+
25
+ DEFAULT_OPTIONS = { :on => :save, :type => :datetime, :allow_nil => false, :allow_blank => false, :ignore_usec => false }
26
+
27
+ attr_reader :configuration, :type
28
+
29
+ def initialize(configuration)
30
+ @configuration = DEFAULT_OPTIONS.merge(configuration)
31
+ @type = @configuration.delete(:type)
32
+ validate_options(@configuration)
33
+ end
34
+
35
+ def call(record, attr_name, value)
36
+ raw_value = raw_value(record, attr_name) || value
37
+
38
+ if value.is_a?(String) || configuration[:format]
39
+ value = ValidatesTimeliness::Parser.parse(raw_value, type, :strict => false, :format => configuration[:format])
40
+ end
41
+
42
+ return if (raw_value.nil? && configuration[:allow_nil]) || (raw_value.blank? && configuration[:allow_blank])
43
+
44
+ return add_error(record, attr_name, :blank) if raw_value.blank?
45
+ return add_error(record, attr_name, "invalid_#{type}".to_sym) if value.nil?
46
+
47
+ validate_restrictions(record, attr_name, value)
48
+ end
49
+
50
+ def error_messages
51
+ @error_messages ||= ::ActiveRecord::Errors.default_error_messages.merge(custom_error_messages)
52
+ end
53
+
54
+ private
55
+
56
+ def raw_value(record, attr_name)
57
+ record.send("#{attr_name}_before_type_cast") rescue nil
58
+ end
59
+
60
+ def validate_restrictions(record, attr_name, value)
61
+ if configuration[:with_time] || configuration[:with_date]
62
+ value = combine_date_and_time(value, record)
63
+ end
64
+
65
+ value = self.class.type_cast_value(value, implied_type, configuration[:ignore_usec])
66
+
67
+ return if value.nil?
68
+
69
+ RESTRICTION_METHODS.each do |option, method|
70
+ next unless restriction = configuration[option]
71
+ begin
72
+ restriction = self.class.evaluate_option_value(restriction, implied_type, record)
73
+ next if restriction.nil?
74
+ restriction = self.class.type_cast_value(restriction, implied_type, configuration[:ignore_usec])
75
+
76
+ unless evaluate_restriction(restriction, value, method)
77
+ add_error(record, attr_name, option, interpolation_values(option, restriction))
78
+ end
79
+ rescue
80
+ unless self.class.ignore_restriction_errors
81
+ add_error(record, attr_name, "restriction '#{option}' value was invalid")
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ def interpolation_values(option, restriction)
88
+ format = self.class.error_value_format_for(type)
89
+ restriction = [restriction] unless restriction.is_a?(Array)
90
+
91
+ if defined?(I18n)
92
+ interpolations = {}
93
+ keys = restriction.size == 1 ? [:restriction] : [:earliest, :latest]
94
+ keys.each_with_index {|key, i| interpolations[key] = restriction[i].strftime(format) }
95
+ interpolations
96
+ else
97
+ restriction.map {|r| r.strftime(format) }
98
+ end
99
+ end
100
+
101
+ def evaluate_restriction(restriction, value, comparator)
102
+ return true if restriction.nil?
103
+
104
+ case comparator
105
+ when Symbol
106
+ value.send(comparator, restriction)
107
+ when Proc
108
+ comparator.call(value, restriction)
109
+ end
110
+ end
111
+
112
+ def add_error(record, attr_name, message, interpolate=nil)
113
+ if defined?(I18n)
114
+ custom = custom_error_messages[message]
115
+ record.errors.add(attr_name, message, { :default => custom }.merge(interpolate || {}))
116
+ else
117
+ message = error_messages[message] if message.is_a?(Symbol)
118
+ record.errors.add(attr_name, message % interpolate)
119
+ end
120
+ end
121
+
122
+ def custom_error_messages
123
+ @custom_error_messages ||= configuration.inject({}) {|msgs, (k, v)|
124
+ if md = /(.*)_message$/.match(k.to_s)
125
+ msgs[md[1].to_sym] = v
126
+ end
127
+ msgs
128
+ }
129
+ end
130
+
131
+ def combine_date_and_time(value, record)
132
+ if type == :date
133
+ date = value
134
+ time = configuration[:with_time]
135
+ else
136
+ date = configuration[:with_date]
137
+ time = value
138
+ end
139
+ date, time = self.class.evaluate_option_value(date, :date, record), self.class.evaluate_option_value(time, :time, record)
140
+ return if date.nil? || time.nil?
141
+ ValidatesTimeliness::Parser.make_time([date.year, date.month, date.day, time.hour, time.min, time.sec, time.usec])
142
+ end
143
+
144
+ def validate_options(options)
145
+ if options.key?(:equal_to)
146
+ ::ActiveSupport::Deprecation.warn("ValidatesTimeliness :equal_to option is deprecated due to clash with a default Rails option. Use :is_at instead. You will need to fix any I18n error message references to this option date/time attributes now.")
147
+ options[:is_at] = options.delete(:equal_to)
148
+ options[:is_at_message] = options.delete(:equal_to_message)
149
+ end
150
+
151
+ invalid_for_type = ([:time, :date, :datetime] - [type]).map {|k| "invalid_#{k}_message".to_sym }
152
+ invalid_for_type << :with_date unless type == :time
153
+ invalid_for_type << :with_time unless type == :date
154
+ options.assert_valid_keys(VALID_OPTION_KEYS - invalid_for_type)
155
+ end
156
+
157
+ def implied_type
158
+ @implied_type ||= configuration[:with_date] || configuration[:with_time] ? :datetime : type
159
+ end
160
+
161
+ # class methods
162
+ class << self
163
+
164
+ def error_value_format_for(type)
165
+ if defined?(I18n)
166
+ # work around for syntax check in vendored I18n for Rails <= 2.3.3
167
+ I18n.t('validates_timeliness.error_value_formats')[type] || error_value_formats[type]
168
+ else
169
+ error_value_formats[type]
170
+ end
171
+ end
172
+
173
+ def evaluate_option_value(value, type, record)
174
+ case value
175
+ when Time, Date
176
+ value
177
+ when Symbol
178
+ evaluate_option_value(record.send(value), type, record)
179
+ when Proc
180
+ result = value.arity > 0 ? value.call(record) : value.call
181
+ evaluate_option_value(result, type, record)
182
+ when Array
183
+ value.map {|r| evaluate_option_value(r, type, record) }.sort
184
+ when Range
185
+ evaluate_option_value([value.first, value.last], type, record)
186
+ else
187
+ ValidatesTimeliness::Parser.parse(value, type, :strict => false)
188
+ end
189
+ end
190
+
191
+ def type_cast_value(value, type, ignore_usec=false)
192
+ if value.is_a?(Array)
193
+ value.map {|v| type_cast_value(v, type, ignore_usec) }
194
+ else
195
+ value = case type
196
+ when :time
197
+ dummy_time(value)
198
+ when :date
199
+ value.to_date
200
+ when :datetime
201
+ if value.is_a?(Time) || value.is_a?(DateTime)
202
+ value.to_time
203
+ else
204
+ value.to_time(ValidatesTimeliness.default_timezone)
205
+ end
206
+ else
207
+ nil
208
+ end
209
+ if ignore_usec && value.is_a?(Time)
210
+ ValidatesTimeliness::Parser.make_time(Array(value).reverse[4..9])
211
+ else
212
+ value
213
+ end
214
+ end
215
+ end
216
+
217
+ def dummy_time(value)
218
+ if value.is_a?(Time) || value.is_a?(DateTime)
219
+ time = [value.hour, value.min, value.sec]
220
+ else
221
+ time = [0,0,0]
222
+ end
223
+ dummy_date = ValidatesTimeliness::Formats.dummy_date_for_time_type
224
+ ValidatesTimeliness::Parser.make_time(dummy_date + time)
225
+ end
226
+
227
+ end
228
+
229
+ end
230
+ end