szilm-validates_timeliness 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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