validates_timeliness 2.3.2 → 3.0.0.beta

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 (59) hide show
  1. data/CHANGELOG +12 -4
  2. data/LICENSE +1 -1
  3. data/README.rdoc +138 -280
  4. data/Rakefile +30 -16
  5. data/lib/generators/validates_timeliness/install_generator.rb +17 -0
  6. data/lib/generators/validates_timeliness/templates/en.yml +16 -0
  7. data/lib/generators/validates_timeliness/templates/validates_timeliness.rb +22 -0
  8. data/lib/validates_timeliness.rb +46 -52
  9. data/lib/validates_timeliness/attribute_methods.rb +51 -0
  10. data/lib/validates_timeliness/conversion.rb +69 -0
  11. data/lib/validates_timeliness/extensions.rb +14 -0
  12. data/lib/validates_timeliness/extensions/date_time_select.rb +45 -0
  13. data/lib/validates_timeliness/extensions/multiparameter_handler.rb +31 -0
  14. data/lib/validates_timeliness/helper_methods.rb +41 -0
  15. data/lib/validates_timeliness/orms/active_record.rb +14 -0
  16. data/lib/validates_timeliness/parser.rb +389 -17
  17. data/lib/validates_timeliness/validator.rb +37 -200
  18. data/lib/validates_timeliness/version.rb +1 -1
  19. data/spec/model_helpers.rb +27 -0
  20. data/spec/spec_helper.rb +74 -43
  21. data/spec/test_model.rb +56 -0
  22. data/spec/validates_timeliness/attribute_methods_spec.rb +36 -0
  23. data/spec/validates_timeliness/conversion_spec.rb +204 -0
  24. data/spec/validates_timeliness/extensions/date_time_select_spec.rb +178 -0
  25. data/spec/validates_timeliness/extensions/multiparameter_handler_spec.rb +21 -0
  26. data/spec/validates_timeliness/helper_methods_spec.rb +36 -0
  27. data/spec/{formats_spec.rb → validates_timeliness/parser_spec.rb} +105 -71
  28. data/spec/validates_timeliness/validator/after_spec.rb +59 -0
  29. data/spec/validates_timeliness/validator/before_spec.rb +59 -0
  30. data/spec/validates_timeliness/validator/is_at_spec.rb +63 -0
  31. data/spec/validates_timeliness/validator/on_or_after_spec.rb +59 -0
  32. data/spec/validates_timeliness/validator/on_or_before_spec.rb +59 -0
  33. data/spec/validates_timeliness/validator_spec.rb +172 -0
  34. data/validates_timeliness.gemspec +30 -0
  35. metadata +42 -40
  36. data/TODO +0 -8
  37. data/lib/validates_timeliness/action_view/instance_tag.rb +0 -52
  38. data/lib/validates_timeliness/active_record/attribute_methods.rb +0 -77
  39. data/lib/validates_timeliness/active_record/multiparameter_attributes.rb +0 -69
  40. data/lib/validates_timeliness/formats.rb +0 -368
  41. data/lib/validates_timeliness/locale/en.new.yml +0 -18
  42. data/lib/validates_timeliness/locale/en.old.yml +0 -18
  43. data/lib/validates_timeliness/matcher.rb +0 -1
  44. data/lib/validates_timeliness/spec/rails/matchers/validate_timeliness.rb +0 -162
  45. data/lib/validates_timeliness/validation_methods.rb +0 -46
  46. data/spec/action_view/instance_tag_spec.rb +0 -194
  47. data/spec/active_record/attribute_methods_spec.rb +0 -157
  48. data/spec/active_record/multiparameter_attributes_spec.rb +0 -118
  49. data/spec/ginger_scenarios.rb +0 -19
  50. data/spec/parser_spec.rb +0 -65
  51. data/spec/resources/application.rb +0 -2
  52. data/spec/resources/person.rb +0 -3
  53. data/spec/resources/schema.rb +0 -10
  54. data/spec/resources/sqlite_patch.rb +0 -19
  55. data/spec/spec/rails/matchers/validate_timeliness_spec.rb +0 -245
  56. data/spec/time_travel/MIT-LICENSE +0 -20
  57. data/spec/time_travel/time_extensions.rb +0 -33
  58. data/spec/time_travel/time_travel.rb +0 -12
  59. data/spec/validator_spec.rb +0 -723
@@ -1,230 +1,67 @@
1
- #TODO remove deprecated option :equal_to
1
+ require 'active_model/validator'
2
+
2
3
  module ValidatesTimeliness
4
+ class Validator < ActiveModel::EachValidator
5
+ include Conversion
3
6
 
4
- class Validator
5
- cattr_accessor :error_value_formats
6
- cattr_accessor :ignore_restriction_errors
7
- self.ignore_restriction_errors = false
7
+ attr_reader :type
8
8
 
9
- RESTRICTION_METHODS = {
9
+ RESTRICTIONS = {
10
10
  :is_at => :==,
11
- :equal_to => :==,
12
11
  :before => :<,
13
12
  :after => :>,
14
13
  :on_or_before => :<=,
15
14
  :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
15
+ }.freeze
28
16
 
29
- def initialize(configuration)
30
- @configuration = DEFAULT_OPTIONS.merge(configuration)
31
- @type = @configuration.delete(:type)
32
- validate_options(@configuration)
17
+ def self.kind
18
+ :timeliness
33
19
  end
34
20
 
35
- def call(record, attr_name, value)
36
- raw_value = raw_value(record, attr_name) || value
21
+ def initialize(options)
22
+ @type = options.delete(:type) || :datetime
23
+ @allow_nil, @allow_blank = options.delete(:allow_nil), options.delete(:allow_blank)
24
+ @restrictions_to_check = RESTRICTIONS.keys & options.keys
37
25
 
38
- if value.is_a?(String) || configuration[:format]
39
- value = ValidatesTimeliness::Parser.parse(raw_value, type, :strict => false, :format => configuration[:format])
26
+ if range = options.delete(:between)
27
+ raise ArgumentError, ":between must be a Range or an Array" unless range.is_a?(Range) || range.is_a?(Array)
28
+ options[:on_or_after], options[:on_or_before] = range.first, range.last
40
29
  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)
30
+ super
48
31
  end
49
32
 
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
33
+ def validate_each(record, attr_name, value)
34
+ raw_value = record._timeliness_raw_value_for(attr_name) || value
35
+ return if (@allow_nil && raw_value.nil?) || (@allow_blank && raw_value.blank?)
64
36
 
65
- value = self.class.type_cast_value(value, implied_type, configuration[:ignore_usec])
37
+ @timezone_aware = record.class.timeliness_attribute_timezone_aware?(attr_name)
38
+ value = parse(value) if value.is_a?(String)
39
+ value = type_cast_value(value, @type)
66
40
 
67
- return if value.nil?
41
+ return record.errors.add(attr_name, :"invalid_#{@type}") if value.blank?
68
42
 
69
- RESTRICTION_METHODS.each do |option, method|
70
- next unless restriction = configuration[option]
43
+ @restrictions_to_check.each do |restriction|
71
44
  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])
45
+ restriction_value = type_cast_value(evaluate_option_value(options[restriction], record), @type)
75
46
 
76
- unless evaluate_restriction(restriction, value, method)
77
- add_error(record, attr_name, option, interpolation_values(option, restriction))
47
+ unless value.send(RESTRICTIONS[restriction], restriction_value)
48
+ return record.errors.add(attr_name, restriction, :message => options[:"#{restriction}_message"], :restriction => format_error_value(restriction_value))
78
49
  end
79
- rescue
80
- unless self.class.ignore_restriction_errors
81
- add_error(record, attr_name, "restriction '#{option}' value was invalid")
50
+ rescue => e
51
+ unless ValidatesTimeliness.ignore_restriction_errors
52
+ record.errors[attr_name] = "Error occurred validating #{attr_name} for #{restriction.inspect} restriction:\n#{e.message}"
82
53
  end
83
54
  end
84
55
  end
85
56
  end
86
57
 
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
-
58
+ def format_error_value(value)
59
+ format = I18n.t(@type, :scope => 'validates_timeliness.error_value_formats')
60
+ value.strftime(format)
227
61
  end
228
62
 
229
63
  end
230
64
  end
65
+
66
+ # Compatibility with ActiveModel validates method which matches option keys to their validator class
67
+ TimelinessValidator = ValidatesTimeliness::Validator
@@ -1,3 +1,3 @@
1
1
  module ValidatesTimeliness
2
- VERSION = "2.3.2"
2
+ VERSION = '3.0.0.beta'
3
3
  end
@@ -0,0 +1,27 @@
1
+ module ModelHelpers
2
+
3
+ # Some test helpers from Rails source
4
+ def invalid!(attr_name, values, error = nil)
5
+ with_each_person_value(attr_name, values) do |record, value|
6
+ record.should be_invalid
7
+ record.errors[attr_name].size.should >= 1
8
+ record.errors[attr_name].first.should == error if error
9
+ end
10
+ end
11
+
12
+ def valid!(attr_name, values)
13
+ with_each_person_value(attr_name, values) do |record, value|
14
+ record.should be_valid
15
+ end
16
+ end
17
+
18
+ def with_each_person_value(attr_name, values)
19
+ record = Person.new
20
+ values = [values] unless values.is_a?(Array)
21
+ values.each do |value|
22
+ record.send("#{attr_name}=", value)
23
+ yield record, value
24
+ end
25
+ end
26
+
27
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,58 +1,89 @@
1
- $:.unshift(File.dirname(__FILE__) + '/../lib')
2
- $:.unshift(File.dirname(__FILE__))
3
- $:.unshift(File.dirname(__FILE__) + '/resources')
1
+ require 'rspec'
2
+ require 'rspec/autorun'
4
3
 
5
- RAILS_ENV = ENV['RAILS_ENV'] = 'test'
6
-
7
- require 'rubygems'
8
- require 'spec/autorun'
4
+ require 'active_model'
5
+ require 'active_model/validations'
6
+ require 'active_record'
7
+ require 'action_view'
8
+ require 'timecop'
9
+ require 'rspec_tag_matchers'
10
+ require 'model_helpers'
9
11
 
10
- vendored_rails = File.dirname(__FILE__) + '/../../../../vendor/rails'
12
+ require 'validates_timeliness'
13
+ require 'test_model'
11
14
 
12
- if vendored = File.exists?(vendored_rails)
13
- Dir.glob(vendored_rails + "/**/lib").each { |dir| $:.unshift dir }
14
- else
15
- begin
16
- require 'ginger'
17
- rescue LoadError
18
- end
19
- if ENV['VERSION']
20
- gem 'rails', ENV['VERSION']
21
- else
22
- gem 'rails'
23
- end
15
+ ValidatesTimeliness.setup do |c|
16
+ c.extend_orms = [ :active_record ]
17
+ c.enable_date_time_select_extension!
18
+ c.enable_multiparameter_extension!
19
+ c.default_timezone = :utc
24
20
  end
25
21
 
26
- RAILS_ROOT = File.dirname(__FILE__)
22
+ Time.zone = 'Australia/Melbourne'
27
23
 
28
- require 'rails/version'
29
- require 'active_record'
30
- require 'active_record/version'
31
- require 'action_controller'
32
- require 'action_view'
33
- require 'action_mailer'
24
+ LOCALE_PATH = File.expand_path(File.dirname(__FILE__) + '/../lib/generators/validates_timeliness/templates/en.yml')
25
+ I18n.load_path.unshift(LOCALE_PATH)
34
26
 
35
- require 'spec/rails'
36
- require 'time_travel/time_travel'
27
+ # Extend TestModel as you would another ORM/ODM module
28
+ module TestModel
29
+ include ValidatesTimeliness::HelperMethods
30
+ include ValidatesTimeliness::AttributeMethods
37
31
 
38
- ActiveRecord::Base.default_timezone = :utc
39
- RAILS_VER = Rails::VERSION::STRING
40
- puts "Using #{vendored ? 'vendored' : 'gem'} Rails version #{RAILS_VER} (ActiveRecord version #{ActiveRecord::VERSION::STRING})"
32
+ def self.included(base)
33
+ base.extend HookMethods
34
+ end
41
35
 
42
- if RAILS_VER >= '2.1'
43
- Time.zone_default = ActiveSupport::TimeZone['UTC']
44
- ActiveRecord::Base.time_zone_aware_attributes = true
45
- end
36
+ module HookMethods
37
+ # Hook method for attribute method generation
38
+ def define_attribute_methods(attr_names)
39
+ super
40
+ define_timeliness_methods
41
+ end
46
42
 
47
- require 'validates_timeliness'
48
- require 'validates_timeliness/matcher'
43
+ # Hook into native time zone handling check, if any
44
+ def timeliness_attribute_timezone_aware?(attr_name)
45
+ false
46
+ end
47
+ end
48
+ end
49
49
 
50
- ValidatesTimeliness.enable_datetime_select_extension!
50
+ class Person
51
+ include TestModel
52
+ self.model_attributes = :birth_date, :birth_time, :birth_datetime
53
+ validates_date :birth_date
54
+ validates_time :birth_time
55
+ validates_datetime :birth_datetime
56
+ define_attribute_methods model_attributes
57
+ end
51
58
 
52
- ActiveRecord::Migration.verbose = false
53
59
  ActiveRecord::Base.establish_connection({:adapter => 'sqlite3', :database => ':memory:'})
60
+ ActiveRecord::Migration.verbose = false
61
+ ActiveRecord::Schema.define(:version => 1) do
62
+ create_table :employees, :force => true do |t|
63
+ t.string :first_name
64
+ t.string :last_name
65
+ t.datetime :birth_date
66
+ t.datetime :birth_time
67
+ t.datetime :birth_datetime
68
+ end
69
+ end
54
70
 
55
- require 'sqlite_patch' if RAILS_VER < '2.1'
71
+ class Employee < ActiveRecord::Base
72
+ validates_date :birth_date
73
+ validates_time :birth_time
74
+ validates_datetime :birth_datetime
75
+ define_attribute_methods
76
+ end
56
77
 
57
- require 'schema'
58
- require 'person'
78
+ Rspec.configure do |c|
79
+ c.mock_with :rspec
80
+ c.include(RspecTagMatchers)
81
+ c.before do
82
+ Person.reset_callbacks(:validate)
83
+ Person.timeliness_validated_attributes = {}
84
+ Person._validators.clear
85
+ Employee.reset_callbacks(:validate)
86
+ Employee.timeliness_validated_attributes = {}
87
+ Employee._validators.clear
88
+ end
89
+ end
@@ -0,0 +1,56 @@
1
+ module TestModel
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ extend ActiveModel::Translation
6
+ include ActiveModel::Validations
7
+ include ActiveModel::AttributeMethods
8
+ include DynamicMethods
9
+
10
+ attribute_method_suffix ""
11
+ attribute_method_suffix "="
12
+ cattr_accessor :model_attributes
13
+ end
14
+
15
+ module ClassMethods
16
+ def define_method_attribute=(attr_name)
17
+ generated_attribute_methods.module_eval("def #{attr_name}=(new_value); @attributes['#{attr_name}']=new_value ; end", __FILE__, __LINE__)
18
+ end
19
+
20
+ def define_method_attribute(attr_name)
21
+ generated_attribute_methods.module_eval("def #{attr_name}; @attributes['#{attr_name}']; end", __FILE__, __LINE__)
22
+ end
23
+ end
24
+
25
+ module DynamicMethods
26
+ def method_missing(method_id, *args, &block)
27
+ if !self.class.attribute_methods_generated?
28
+ self.class.define_attribute_methods self.class.model_attributes.map(&:to_s)
29
+ method_name = method_id.to_s
30
+ send(method_id, *args, &block)
31
+ else
32
+ super
33
+ end
34
+ end
35
+ end
36
+
37
+ def initialize(attributes = nil)
38
+ @attributes = self.class.model_attributes.inject({}) do |hash, column|
39
+ hash[column.to_s] = nil
40
+ hash
41
+ end
42
+ self.attributes = attributes unless attributes.nil?
43
+ end
44
+
45
+ def attributes
46
+ @attributes.keys
47
+ end
48
+
49
+ def attributes=(new_attributes={})
50
+ new_attributes.each do |key, value|
51
+ send "#{key}=", value
52
+ end
53
+ end
54
+
55
+ end
56
+