validates_timeliness 2.3.2 → 3.0.0.beta

Sign up to get free protection for your applications and to get access to all the features.
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
+