markos_validates_timeliness 2.3.2

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 (35) hide show
  1. data/CHANGELOG +121 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +402 -0
  4. data/Rakefile +52 -0
  5. data/TODO +8 -0
  6. data/lib/validates_timeliness/action_view/instance_tag.rb +52 -0
  7. data/lib/validates_timeliness/active_record/attribute_methods.rb +77 -0
  8. data/lib/validates_timeliness/active_record/multiparameter_attributes.rb +69 -0
  9. data/lib/validates_timeliness/formats.rb +368 -0
  10. data/lib/validates_timeliness/locale/en.new.yml +18 -0
  11. data/lib/validates_timeliness/locale/en.old.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/lib/validates_timeliness.rb +59 -0
  19. data/spec/action_view/instance_tag_spec.rb +194 -0
  20. data/spec/active_record/attribute_methods_spec.rb +157 -0
  21. data/spec/active_record/multiparameter_attributes_spec.rb +118 -0
  22. data/spec/formats_spec.rb +313 -0
  23. data/spec/ginger_scenarios.rb +19 -0
  24. data/spec/parser_spec.rb +65 -0
  25. data/spec/resources/application.rb +2 -0
  26. data/spec/resources/person.rb +3 -0
  27. data/spec/resources/schema.rb +10 -0
  28. data/spec/resources/sqlite_patch.rb +19 -0
  29. data/spec/spec/rails/matchers/validate_timeliness_spec.rb +245 -0
  30. data/spec/spec_helper.rb +58 -0
  31. data/spec/time_travel/MIT-LICENSE +20 -0
  32. data/spec/time_travel/time_extensions.rb +33 -0
  33. data/spec/time_travel/time_travel.rb +12 -0
  34. data/spec/validator_spec.rb +723 -0
  35. metadata +104 -0
@@ -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
@@ -0,0 +1,3 @@
1
+ module ValidatesTimeliness
2
+ VERSION = "2.3.2"
3
+ end
@@ -0,0 +1,59 @@
1
+ require 'validates_timeliness/formats'
2
+ require 'validates_timeliness/parser'
3
+ require 'validates_timeliness/validator'
4
+ require 'validates_timeliness/validation_methods'
5
+ require 'validates_timeliness/active_record/attribute_methods'
6
+ require 'validates_timeliness/active_record/multiparameter_attributes'
7
+ require 'validates_timeliness/action_view/instance_tag'
8
+ begin
9
+ i18n_path = $:.grep(/active_support\/vendor\/i18n-/)
10
+ if i18n_path.empty?
11
+ require 'i18n/version'
12
+ else
13
+ require i18n_path[0] + '/version'
14
+ end
15
+ rescue LoadError
16
+ end if defined?(I18n)
17
+
18
+ module ValidatesTimeliness
19
+
20
+ mattr_accessor :default_timezone
21
+ self.default_timezone = :utc
22
+
23
+ mattr_accessor :use_time_zones
24
+ self.use_time_zones = false
25
+
26
+ I18N_LATEST = defined?(I18n::VERSION) && I18n::VERSION >= '0.4.0'
27
+ locale_file = I18N_LATEST ? 'en.new.yml' : 'en.old.yml'
28
+ LOCALE_PATH = File.expand_path(File.join(File.dirname(__FILE__),'validates_timeliness','locale',locale_file))
29
+
30
+ class << self
31
+
32
+ def enable_datetime_select_extension!
33
+ enable_datetime_select_invalid_value_extension!
34
+ enable_multiparameter_attributes_extension!
35
+ end
36
+
37
+ def load_error_messages
38
+ defaults = YAML::load(IO.read(LOCALE_PATH))['en']
39
+ ValidatesTimeliness::Validator.error_value_formats = defaults['validates_timeliness']['error_value_formats'].symbolize_keys
40
+
41
+ if defined?(I18n)
42
+ I18n.load_path.unshift(LOCALE_PATH)
43
+ I18n.reload!
44
+ else
45
+ errors = defaults['activerecord']['errors']['messages'].inject({}) {|h,(k,v)| h[k.to_sym] = v.gsub(/\{\{\w*\}\}/, '%s');h }
46
+ ::ActiveRecord::Errors.default_error_messages.update(errors)
47
+ end
48
+ end
49
+
50
+ def setup_for_rails
51
+ self.default_timezone = ::ActiveRecord::Base.default_timezone
52
+ self.use_time_zones = ::ActiveRecord::Base.time_zone_aware_attributes rescue false
53
+ self.enable_active_record_datetime_parser!
54
+ load_error_messages
55
+ end
56
+ end
57
+ end
58
+
59
+ ValidatesTimeliness.setup_for_rails