validates_timeliness 4.1.1 → 6.0.0.beta2
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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +7 -0
- data/.github/workflows/ci.yml +41 -0
- data/CHANGELOG.rdoc +14 -6
- data/LICENSE +1 -1
- data/README.md +321 -0
- data/Rakefile +0 -10
- data/gemfiles/rails_6_0.gemfile +14 -0
- data/gemfiles/rails_6_1.gemfile +14 -0
- data/gemfiles/rails_edge.gemfile +14 -0
- data/lib/validates_timeliness/attribute_methods.rb +34 -73
- data/lib/validates_timeliness/{conversion.rb → converter.rb} +26 -14
- data/lib/validates_timeliness/extensions/date_time_select.rb +23 -27
- data/lib/validates_timeliness/extensions/multiparameter_handler.rb +47 -66
- data/lib/validates_timeliness/extensions.rb +2 -2
- data/lib/validates_timeliness/orm/active_model.rb +53 -2
- data/lib/validates_timeliness/orm/active_record.rb +2 -84
- data/lib/validates_timeliness/railtie.rb +1 -1
- data/lib/validates_timeliness/validator.rb +21 -18
- data/lib/validates_timeliness/version.rb +1 -1
- data/lib/validates_timeliness.rb +3 -3
- data/spec/spec_helper.rb +2 -0
- data/spec/support/model_helpers.rb +1 -2
- data/spec/support/test_model.rb +6 -4
- data/spec/validates_timeliness/attribute_methods_spec.rb +0 -15
- data/spec/validates_timeliness/{conversion_spec.rb → converter_spec.rb} +64 -49
- data/spec/validates_timeliness/extensions/date_time_select_spec.rb +27 -27
- data/spec/validates_timeliness/extensions/multiparameter_handler_spec.rb +10 -10
- data/spec/validates_timeliness/orm/active_record_spec.rb +72 -136
- data/validates_timeliness.gemspec +4 -3
- metadata +38 -16
- data/.travis.yml +0 -24
- data/README.rdoc +0 -300
- data/gemfiles/rails_4_2.gemfile +0 -14
@@ -1,10 +1,18 @@
|
|
1
1
|
module ValidatesTimeliness
|
2
|
-
|
2
|
+
class Converter
|
3
|
+
attr_reader :type, :format, :ignore_usec
|
3
4
|
|
4
|
-
def
|
5
|
+
def initialize(type:, format: nil, ignore_usec: false, time_zone_aware: false)
|
6
|
+
@type = type
|
7
|
+
@format = format
|
8
|
+
@ignore_usec = ignore_usec
|
9
|
+
@time_zone_aware = time_zone_aware
|
10
|
+
end
|
11
|
+
|
12
|
+
def type_cast_value(value)
|
5
13
|
return nil if value.nil? || !value.respond_to?(:to_time)
|
6
14
|
|
7
|
-
value = value.in_time_zone if value.acts_like?(:time) &&
|
15
|
+
value = value.in_time_zone if value.acts_like?(:time) && time_zone_aware?
|
8
16
|
value = case type
|
9
17
|
when :time
|
10
18
|
dummy_time(value)
|
@@ -15,8 +23,8 @@ module ValidatesTimeliness
|
|
15
23
|
else
|
16
24
|
value
|
17
25
|
end
|
18
|
-
if
|
19
|
-
Timeliness::Parser.make_time(Array(value).reverse[4..9], (:current if
|
26
|
+
if ignore_usec && value.is_a?(Time)
|
27
|
+
Timeliness::Parser.make_time(Array(value).reverse[4..9], (:current if time_zone_aware?))
|
20
28
|
else
|
21
29
|
value
|
22
30
|
end
|
@@ -24,30 +32,30 @@ module ValidatesTimeliness
|
|
24
32
|
|
25
33
|
def dummy_time(value)
|
26
34
|
time = if value.acts_like?(:time)
|
27
|
-
value = value.in_time_zone if
|
35
|
+
value = value.in_time_zone if time_zone_aware?
|
28
36
|
[value.hour, value.min, value.sec]
|
29
37
|
else
|
30
38
|
[0,0,0]
|
31
39
|
end
|
32
40
|
values = ValidatesTimeliness.dummy_date_for_time_type + time
|
33
|
-
Timeliness::Parser.make_time(values, (:current if
|
41
|
+
Timeliness::Parser.make_time(values, (:current if time_zone_aware?))
|
34
42
|
end
|
35
43
|
|
36
|
-
def
|
44
|
+
def evaluate(value, scope=nil)
|
37
45
|
case value
|
38
46
|
when Time, Date
|
39
47
|
value
|
40
48
|
when String
|
41
49
|
parse(value)
|
42
50
|
when Symbol
|
43
|
-
if !
|
51
|
+
if !scope.respond_to?(value) && restriction_shorthand?(value)
|
44
52
|
ValidatesTimeliness.restriction_shorthand_symbols[value].call
|
45
53
|
else
|
46
|
-
|
54
|
+
evaluate(scope.send(value))
|
47
55
|
end
|
48
56
|
when Proc
|
49
|
-
result = value.arity > 0 ? value.call(
|
50
|
-
|
57
|
+
result = value.arity > 0 ? value.call(scope) : value.call
|
58
|
+
evaluate(result, scope)
|
51
59
|
else
|
52
60
|
value
|
53
61
|
end
|
@@ -59,14 +67,18 @@ module ValidatesTimeliness
|
|
59
67
|
|
60
68
|
def parse(value)
|
61
69
|
return nil if value.nil?
|
70
|
+
|
62
71
|
if ValidatesTimeliness.use_plugin_parser
|
63
|
-
Timeliness::Parser.parse(value,
|
72
|
+
Timeliness::Parser.parse(value, type, zone: (:current if time_zone_aware?), format: format, strict: false)
|
64
73
|
else
|
65
|
-
|
74
|
+
time_zone_aware? ? Time.zone.parse(value) : value.to_time(ValidatesTimeliness.default_timezone)
|
66
75
|
end
|
67
76
|
rescue ArgumentError, TypeError
|
68
77
|
nil
|
69
78
|
end
|
70
79
|
|
80
|
+
def time_zone_aware?
|
81
|
+
@time_zone_aware
|
82
|
+
end
|
71
83
|
end
|
72
84
|
end
|
@@ -1,54 +1,50 @@
|
|
1
1
|
module ValidatesTimeliness
|
2
2
|
module Extensions
|
3
|
-
module
|
4
|
-
extend ActiveSupport::Concern
|
5
|
-
|
3
|
+
module TimelinessDateTimeSelect
|
6
4
|
# Intercepts the date and time select helpers to reuse the values from
|
7
5
|
# the params rather than the parsed value. This allows invalid date/time
|
8
6
|
# values to be redisplayed instead of blanks to aid correction by the user.
|
9
7
|
# It's a minor usability improvement which is rarely an issue for the user.
|
8
|
+
attr_accessor :object_name, :method_name, :template_object, :options, :html_options
|
10
9
|
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
POSITION = {
|
11
|
+
:year => 1, :month => 2, :day => 3, :hour => 4, :min => 5, :sec => 6
|
12
|
+
}.freeze
|
14
13
|
|
15
|
-
class
|
14
|
+
class DateTimeValue
|
16
15
|
attr_accessor :year, :month, :day, :hour, :min, :sec
|
17
16
|
|
18
|
-
def initialize(year
|
17
|
+
def initialize(year:, month:, day: nil, hour: nil, min: nil, sec: nil)
|
19
18
|
@year, @month, @day, @hour, @min, @sec = year, month, day, hour, min, sec
|
20
19
|
end
|
21
20
|
|
22
|
-
# adapted from activesupport/lib/active_support/core_ext/date_time/calculations.rb, line 36 (3.0.7)
|
23
21
|
def change(options)
|
24
|
-
|
25
|
-
options
|
26
|
-
options
|
27
|
-
options
|
28
|
-
options
|
29
|
-
options
|
30
|
-
options
|
22
|
+
self.class.new(
|
23
|
+
year: options.fetch(:year, year),
|
24
|
+
month: options.fetch(:month, month),
|
25
|
+
day: options.fetch(:day, day),
|
26
|
+
hour: options.fetch(:hour, hour),
|
27
|
+
min: options.fetch(:min) { options[:hour] ? 0 : min },
|
28
|
+
sec: options.fetch(:sec) { options[:hour] || options[:min] ? 0 : sec }
|
31
29
|
)
|
32
30
|
end
|
33
31
|
end
|
34
32
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
@template_object.params[@object_name]
|
33
|
+
# Splat args to support Rails 5.0 which expects object, and 5.2 which doesn't
|
34
|
+
def value(*object)
|
35
|
+
return super unless @template_object.params[@object_name]
|
39
36
|
|
40
37
|
pairs = @template_object.params[@object_name].select {|k,v| k =~ /^#{@method_name}\(/ }
|
41
|
-
return
|
38
|
+
return super if pairs.empty?
|
42
39
|
|
43
|
-
values =
|
44
|
-
pairs.
|
45
|
-
position =
|
46
|
-
values[position.to_i
|
40
|
+
values = {}
|
41
|
+
pairs.each_pair do |key, value|
|
42
|
+
position = key[/\((\d+)\w+\)/, 1]
|
43
|
+
values[POSITION.key(position.to_i)] = value.to_i
|
47
44
|
end
|
48
45
|
|
49
|
-
|
46
|
+
DateTimeValue.new(**values)
|
50
47
|
end
|
51
|
-
|
52
48
|
end
|
53
49
|
end
|
54
50
|
end
|
@@ -1,74 +1,55 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
module ValidatesTimeliness
|
2
|
+
module Extensions
|
3
|
+
class AcceptsMultiparameterTime < Module
|
4
|
+
|
5
|
+
def initialize(defaults: {})
|
6
|
+
|
7
|
+
define_method(:cast) do |value|
|
8
|
+
if value.is_a?(Hash)
|
9
|
+
value_from_multiparameter_assignment(value)
|
10
|
+
else
|
11
|
+
super(value)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
define_method(:assert_valid_value) do |value|
|
16
|
+
if value.is_a?(Hash)
|
17
|
+
value_from_multiparameter_assignment(value)
|
18
|
+
else
|
19
|
+
super(value)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
define_method(:value_from_multiparameter_assignment) do |values_hash|
|
24
|
+
defaults.each do |k, v|
|
25
|
+
values_hash[k] ||= v
|
26
|
+
end
|
27
|
+
return unless values_hash.values_at(1,2,3).all?{ |v| v.present? } &&
|
28
|
+
Date.valid_civil?(*values_hash.values_at(1,2,3))
|
29
|
+
|
30
|
+
values = values_hash.sort.map(&:last)
|
31
|
+
::Time.send(default_timezone, *values)
|
32
|
+
end
|
33
|
+
private :value_from_multiparameter_assignment
|
3
34
|
|
4
|
-
# Yield if date values are valid
|
5
|
-
def validate_multiparameter_date_values(set_values)
|
6
|
-
if set_values[0..2].all?{ |v| v.present? } && Date.valid_civil?(*set_values[0..2])
|
7
|
-
yield
|
8
|
-
else
|
9
|
-
invalid_multiparameter_date_or_time_as_string(set_values)
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
|
-
def invalid_multiparameter_date_or_time_as_string(values)
|
14
|
-
value = [values[0], *values[1..2].map {|s| s.to_s.rjust(2,"0")} ].join("-")
|
15
|
-
value += ' ' + values[3..5].map {|s| s.to_s.rjust(2, "0") }.join(":") unless values[3..5].empty?
|
16
|
-
value
|
17
|
-
end
|
18
|
-
|
19
|
-
def instantiate_time_object(set_values)
|
20
|
-
raise if set_values.any?(&:nil?)
|
21
|
-
|
22
|
-
validate_multiparameter_date_values(set_values) {
|
23
|
-
set_values = set_values.map {|v| v.is_a?(String) ? v.strip : v }
|
24
|
-
|
25
|
-
if object.class.send(:create_time_zone_conversion_attribute?, name, cast_type_or_column)
|
26
|
-
Time.zone.local(*set_values)
|
27
|
-
else
|
28
|
-
Time.send(object.class.default_timezone, *set_values)
|
29
|
-
end
|
30
|
-
}
|
31
|
-
rescue
|
32
|
-
invalid_multiparameter_date_or_time_as_string(set_values)
|
33
|
-
end
|
34
|
-
|
35
|
-
def read_time
|
36
|
-
# If column is a :time (and not :date or :timestamp) there is no need to validate if
|
37
|
-
# there are year/month/day fields
|
38
|
-
if cast_type_or_column.type == :time
|
39
|
-
# if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil
|
40
|
-
{ 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value|
|
41
|
-
values[key] ||= value
|
42
35
|
end
|
43
|
-
end
|
44
|
-
|
45
|
-
max_position = extract_max_param(6)
|
46
|
-
set_values = values.values_at(*(1..max_position))
|
47
36
|
|
48
|
-
instantiate_time_object(set_values)
|
49
|
-
end
|
50
|
-
|
51
|
-
def read_date
|
52
|
-
set_values = values.values_at(1,2,3).map {|v| v.is_a?(String) ? v.strip : v }
|
53
|
-
|
54
|
-
if set_values.any? { |v| v.is_a?(String) }
|
55
|
-
Timeliness.parse(set_values.join('-'), :date).try(:to_date) or raise TypeError
|
56
|
-
else
|
57
|
-
Date.new(*set_values)
|
58
37
|
end
|
59
|
-
rescue TypeError, ArgumentError, NoMethodError => ex # if Date.new raises an exception on an invalid date
|
60
|
-
# Date.new with nil values throws NoMethodError
|
61
|
-
raise ex if ex.is_a?(NoMethodError) && ex.message !~ /undefined method `div' for/
|
62
|
-
invalid_multiparameter_date_or_time_as_string(set_values)
|
63
|
-
end
|
64
|
-
|
65
|
-
# Cast type is v4.2 and column before
|
66
|
-
def cast_type_or_column
|
67
|
-
@cast_type || @column
|
68
38
|
end
|
39
|
+
end
|
69
40
|
|
70
|
-
|
71
|
-
|
72
|
-
|
41
|
+
ActiveModel::Type::Date.class_eval do
|
42
|
+
include ValidatesTimeliness::Extensions::AcceptsMultiparameterTime.new
|
43
|
+
end
|
73
44
|
|
45
|
+
ActiveModel::Type::Time.class_eval do
|
46
|
+
include ValidatesTimeliness::Extensions::AcceptsMultiparameterTime.new(
|
47
|
+
defaults: { 1 => 1970, 2 => 1, 3 => 1, 4 => 0, 5 => 0 }
|
48
|
+
)
|
74
49
|
end
|
50
|
+
|
51
|
+
ActiveModel::Type::DateTime.class_eval do
|
52
|
+
include ValidatesTimeliness::Extensions::AcceptsMultiparameterTime.new(
|
53
|
+
defaults: { 4 => 0, 5 => 0 }
|
54
|
+
)
|
55
|
+
end
|
@@ -1,10 +1,10 @@
|
|
1
1
|
module ValidatesTimeliness
|
2
2
|
module Extensions
|
3
|
-
autoload :
|
3
|
+
autoload :TimelinessDateTimeSelect, 'validates_timeliness/extensions/date_time_select'
|
4
4
|
end
|
5
5
|
|
6
6
|
def self.enable_date_time_select_extension!
|
7
|
-
::ActionView::Helpers::Tags::DateSelect.send(:
|
7
|
+
::ActionView::Helpers::Tags::DateSelect.send(:prepend, ValidatesTimeliness::Extensions::TimelinessDateTimeSelect)
|
8
8
|
end
|
9
9
|
|
10
10
|
def self.enable_multiparameter_extension!
|
@@ -7,14 +7,65 @@ module ValidatesTimeliness
|
|
7
7
|
public
|
8
8
|
|
9
9
|
def define_attribute_methods(*attr_names)
|
10
|
-
super.tap { define_timeliness_methods}
|
10
|
+
super.tap { define_timeliness_methods }
|
11
11
|
end
|
12
12
|
|
13
13
|
def undefine_attribute_methods
|
14
14
|
super.tap { undefine_timeliness_attribute_methods }
|
15
15
|
end
|
16
|
+
|
17
|
+
def define_timeliness_methods(before_type_cast=false)
|
18
|
+
return if timeliness_validated_attributes.blank?
|
19
|
+
timeliness_validated_attributes.each do |attr_name|
|
20
|
+
define_attribute_timeliness_methods(attr_name, before_type_cast)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def generated_timeliness_methods
|
25
|
+
@generated_timeliness_methods ||= Module.new { |m|
|
26
|
+
extend Mutex_m
|
27
|
+
}.tap { |mod| include mod }
|
28
|
+
end
|
29
|
+
|
30
|
+
def undefine_timeliness_attribute_methods
|
31
|
+
generated_timeliness_methods.module_eval do
|
32
|
+
instance_methods.each { |m| undef_method(m) }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def define_attribute_timeliness_methods(attr_name, before_type_cast=false)
|
37
|
+
define_timeliness_write_method(attr_name)
|
38
|
+
define_timeliness_before_type_cast_method(attr_name) if before_type_cast
|
39
|
+
end
|
40
|
+
|
41
|
+
def define_timeliness_write_method(attr_name)
|
42
|
+
generated_timeliness_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
|
43
|
+
def #{attr_name}=(value)
|
44
|
+
@timeliness_cache ||= {}
|
45
|
+
@timeliness_cache['#{attr_name}'] = value
|
46
|
+
|
47
|
+
@attributes['#{attr_name}'] = super
|
48
|
+
end
|
49
|
+
STR
|
50
|
+
end
|
51
|
+
|
52
|
+
def define_timeliness_before_type_cast_method(attr_name)
|
53
|
+
generated_timeliness_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
|
54
|
+
def #{attr_name}_before_type_cast
|
55
|
+
read_timeliness_attribute_before_type_cast('#{attr_name}')
|
56
|
+
end
|
57
|
+
STR
|
58
|
+
end
|
16
59
|
end
|
17
|
-
|
60
|
+
|
61
|
+
def read_timeliness_attribute_before_type_cast(attr_name)
|
62
|
+
@timeliness_cache && @timeliness_cache[attr_name] || @attributes[attr_name]
|
63
|
+
end
|
64
|
+
|
65
|
+
def _clear_timeliness_cache
|
66
|
+
@timeliness_cache = {}
|
67
|
+
end
|
68
|
+
|
18
69
|
end
|
19
70
|
end
|
20
71
|
end
|
@@ -3,97 +3,15 @@ module ValidatesTimeliness
|
|
3
3
|
module ActiveRecord
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
|
6
|
-
module ClassMethods
|
7
|
-
public
|
8
|
-
|
9
|
-
def timeliness_attribute_timezone_aware?(attr_name)
|
10
|
-
create_time_zone_conversion_attribute?(attr_name, timeliness_column_for_attribute(attr_name))
|
11
|
-
end
|
12
|
-
|
13
|
-
def timeliness_attribute_type(attr_name)
|
14
|
-
timeliness_column_for_attribute(attr_name).type
|
15
|
-
end
|
16
|
-
|
17
|
-
if ::ActiveModel.version >= Gem::Version.new('4.2')
|
18
|
-
def timeliness_column_for_attribute(attr_name)
|
19
|
-
columns_hash.fetch(attr_name.to_s) do |key|
|
20
|
-
validation_type = _validators[key.to_sym].find {|v| v.kind == :timeliness }.type.to_s
|
21
|
-
::ActiveRecord::ConnectionAdapters::Column.new(key, nil, lookup_cast_type(validation_type), validation_type)
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
def lookup_cast_type(sql_type)
|
26
|
-
case sql_type
|
27
|
-
when 'datetime' then ::ActiveRecord::Type::DateTime.new
|
28
|
-
when 'date' then ::ActiveRecord::Type::Date.new
|
29
|
-
when 'time' then ::ActiveRecord::Type::Time.new
|
30
|
-
end
|
31
|
-
end
|
32
|
-
else
|
33
|
-
def timeliness_column_for_attribute(attr_name)
|
34
|
-
columns_hash.fetch(attr_name.to_s) do |key|
|
35
|
-
validation_type = _validators[key.to_sym].find {|v| v.kind == :timeliness }.type.to_s
|
36
|
-
::ActiveRecord::ConnectionAdapters::Column.new(key, nil, validation_type)
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
def define_attribute_methods
|
42
|
-
super.tap {
|
43
|
-
generated_timeliness_methods.synchronize do
|
44
|
-
return if @timeliness_methods_generated
|
45
|
-
define_timeliness_methods true
|
46
|
-
@timeliness_methods_generated = true
|
47
|
-
end
|
48
|
-
}
|
49
|
-
end
|
50
|
-
|
51
|
-
def undefine_attribute_methods
|
52
|
-
super.tap {
|
53
|
-
generated_timeliness_methods.synchronize do
|
54
|
-
return unless @timeliness_methods_generated
|
55
|
-
undefine_timeliness_attribute_methods
|
56
|
-
@timeliness_methods_generated = false
|
57
|
-
end
|
58
|
-
}
|
59
|
-
end
|
60
|
-
# Override to overwrite methods in ActiveRecord attribute method module because in AR 4+
|
61
|
-
# there is curious code which calls the method directly from the generated methods module
|
62
|
-
# via bind inside method_missing. This means our method in the formerly custom timeliness
|
63
|
-
# methods module was never reached.
|
64
|
-
def generated_timeliness_methods
|
65
|
-
generated_attribute_methods
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
def write_timeliness_attribute(attr_name, value)
|
70
|
-
@timeliness_cache ||= {}
|
71
|
-
@timeliness_cache[attr_name] = value
|
72
|
-
|
73
|
-
if ValidatesTimeliness.use_plugin_parser
|
74
|
-
type = self.class.timeliness_attribute_type(attr_name)
|
75
|
-
timezone = :current if self.class.timeliness_attribute_timezone_aware?(attr_name)
|
76
|
-
value = Timeliness::Parser.parse(value, type, :zone => timezone)
|
77
|
-
value = value.to_date if value && type == :date
|
78
|
-
end
|
79
|
-
|
80
|
-
write_attribute(attr_name, value)
|
81
|
-
end
|
82
|
-
|
83
6
|
def read_timeliness_attribute_before_type_cast(attr_name)
|
84
|
-
|
85
|
-
end
|
86
|
-
|
87
|
-
def reload(*args)
|
88
|
-
_clear_timeliness_cache
|
89
|
-
super
|
7
|
+
read_attribute_before_type_cast(attr_name)
|
90
8
|
end
|
91
9
|
|
92
10
|
end
|
93
11
|
end
|
94
12
|
end
|
95
13
|
|
96
|
-
|
14
|
+
ActiveSupport.on_load(:active_record) do
|
97
15
|
include ValidatesTimeliness::AttributeMethods
|
98
16
|
include ValidatesTimeliness::ORM::ActiveRecord
|
99
17
|
end
|
@@ -12,7 +12,7 @@ module ValidatesTimeliness
|
|
12
12
|
ValidatesTimeliness.ignore_restriction_errors = !Rails.env.test?
|
13
13
|
end
|
14
14
|
|
15
|
-
initializer "validates_timeliness.initialize_timeliness_ambiguous_date_format", :after =>
|
15
|
+
initializer "validates_timeliness.initialize_timeliness_ambiguous_date_format", :after => :load_config_initializers do
|
16
16
|
if Timeliness.respond_to?(:ambiguous_date_format) # i.e. v0.4+
|
17
17
|
# Set default for each new thread if you have changed the default using
|
18
18
|
# the format switching methods.
|
@@ -3,9 +3,7 @@ require 'active_model/validator'
|
|
3
3
|
|
4
4
|
module ValidatesTimeliness
|
5
5
|
class Validator < ActiveModel::EachValidator
|
6
|
-
|
7
|
-
|
8
|
-
attr_reader :type, :attributes
|
6
|
+
attr_reader :type, :attributes, :converter
|
9
7
|
|
10
8
|
RESTRICTIONS = {
|
11
9
|
:is_at => :==,
|
@@ -55,18 +53,14 @@ module ValidatesTimeliness
|
|
55
53
|
end
|
56
54
|
end
|
57
55
|
|
58
|
-
# Rails 4.0 compatibility for old #setup method with class as arg
|
59
|
-
if Gem::Version.new(ActiveModel::VERSION::STRING) <= Gem::Version.new('4.1')
|
60
|
-
alias_method(:setup, :setup_timeliness_validated_attributes)
|
61
|
-
end
|
62
|
-
|
63
56
|
def validate_each(record, attr_name, value)
|
64
57
|
raw_value = attribute_raw_value(record, attr_name) || value
|
65
58
|
return if (@allow_nil && raw_value.nil?) || (@allow_blank && raw_value.blank?)
|
66
59
|
|
67
|
-
@
|
68
|
-
|
69
|
-
value =
|
60
|
+
@converter = initialize_converter(record, attr_name)
|
61
|
+
|
62
|
+
value = @converter.parse(raw_value) if value.is_a?(String) || options[:format]
|
63
|
+
value = @converter.type_cast_value(value)
|
70
64
|
|
71
65
|
add_error(record, attr_name, :"invalid_#{@type}") and return if value.blank?
|
72
66
|
|
@@ -76,7 +70,7 @@ module ValidatesTimeliness
|
|
76
70
|
def validate_restrictions(record, attr_name, value)
|
77
71
|
@restrictions_to_check.each do |restriction|
|
78
72
|
begin
|
79
|
-
restriction_value = type_cast_value(
|
73
|
+
restriction_value = @converter.type_cast_value(@converter.evaluate(options[restriction], record))
|
80
74
|
unless value.send(RESTRICTIONS[restriction], restriction_value)
|
81
75
|
add_error(record, attr_name, restriction, restriction_value) and break
|
82
76
|
end
|
@@ -91,12 +85,12 @@ module ValidatesTimeliness
|
|
91
85
|
|
92
86
|
def add_error(record, attr_name, message, value=nil)
|
93
87
|
value = format_error_value(value) if value
|
94
|
-
message_options = { :
|
95
|
-
record.errors.add(attr_name, message, message_options)
|
88
|
+
message_options = { message: options.fetch(:"#{message}_message", options[:message]), restriction: value }
|
89
|
+
record.errors.add(attr_name, message, **message_options)
|
96
90
|
end
|
97
91
|
|
98
92
|
def format_error_value(value)
|
99
|
-
format = I18n.t(@type, :
|
93
|
+
format = I18n.t(@type, default: DEFAULT_ERROR_VALUE_FORMATS[@type], scope: 'validates_timeliness.error_value_formats')
|
100
94
|
value.strftime(format)
|
101
95
|
end
|
102
96
|
|
@@ -105,9 +99,18 @@ module ValidatesTimeliness
|
|
105
99
|
record.read_timeliness_attribute_before_type_cast(attr_name.to_s)
|
106
100
|
end
|
107
101
|
|
108
|
-
def
|
109
|
-
record.class.respond_to?(:
|
110
|
-
record.class.
|
102
|
+
def time_zone_aware?(record, attr_name)
|
103
|
+
record.class.respond_to?(:skip_time_zone_conversion_for_attributes) &&
|
104
|
+
!record.class.skip_time_zone_conversion_for_attributes.include?(attr_name.to_sym)
|
105
|
+
end
|
106
|
+
|
107
|
+
def initialize_converter(record, attr_name)
|
108
|
+
ValidatesTimeliness::Converter.new(
|
109
|
+
type: @type,
|
110
|
+
time_zone_aware: time_zone_aware?(record, attr_name),
|
111
|
+
format: options[:format],
|
112
|
+
ignore_usec: options[:ignore_usec]
|
113
|
+
)
|
111
114
|
end
|
112
115
|
|
113
116
|
end
|
data/lib/validates_timeliness.rb
CHANGED
@@ -36,8 +36,8 @@ module ValidatesTimeliness
|
|
36
36
|
|
37
37
|
# Shorthand time and date symbols for restrictions
|
38
38
|
self.restriction_shorthand_symbols = {
|
39
|
-
:
|
40
|
-
:
|
39
|
+
now: proc { Time.current },
|
40
|
+
today: proc { Date.current }
|
41
41
|
}
|
42
42
|
|
43
43
|
# Use the plugin date/time parser which is stricter and extensible
|
@@ -62,7 +62,7 @@ module ValidatesTimeliness
|
|
62
62
|
def self.parser; Timeliness end
|
63
63
|
end
|
64
64
|
|
65
|
-
require 'validates_timeliness/
|
65
|
+
require 'validates_timeliness/converter'
|
66
66
|
require 'validates_timeliness/validator'
|
67
67
|
require 'validates_timeliness/helper_methods'
|
68
68
|
require 'validates_timeliness/attribute_methods'
|
data/spec/spec_helper.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'rspec'
|
2
2
|
|
3
|
+
require 'byebug'
|
3
4
|
require 'active_model'
|
4
5
|
require 'active_model/validations'
|
5
6
|
require 'active_record'
|
@@ -59,6 +60,7 @@ end
|
|
59
60
|
ActiveRecord::Base.default_timezone = :utc
|
60
61
|
ActiveRecord::Base.time_zone_aware_attributes = true
|
61
62
|
ActiveRecord::Base.establish_connection({:adapter => 'sqlite3', :database => ':memory:'})
|
63
|
+
ActiveRecord::Base.time_zone_aware_types = [:datetime, :time]
|
62
64
|
ActiveRecord::Migration.verbose = false
|
63
65
|
ActiveRecord::Schema.define(:version => 1) do
|
64
66
|
create_table :employees, :force => true do |t|
|
@@ -17,8 +17,7 @@ module ModelHelpers
|
|
17
17
|
|
18
18
|
def with_each_person_value(attr_name, values)
|
19
19
|
record = Person.new
|
20
|
-
values
|
21
|
-
values.each do |value|
|
20
|
+
Array.wrap(values).each do |value|
|
22
21
|
record.send("#{attr_name}=", value)
|
23
22
|
yield record, value
|
24
23
|
end
|
data/spec/support/test_model.rb
CHANGED
@@ -15,17 +15,19 @@ module TestModel
|
|
15
15
|
self.model_attributes[name] = type
|
16
16
|
end
|
17
17
|
|
18
|
-
def define_method_attribute=(attr_name)
|
18
|
+
def define_method_attribute=(attr_name, owner: nil)
|
19
19
|
generated_attribute_methods.module_eval("def #{attr_name}=(new_value); @attributes['#{attr_name}']=self.class.type_cast('#{attr_name}', new_value); end", __FILE__, __LINE__)
|
20
20
|
end
|
21
21
|
|
22
|
-
def define_method_attribute(attr_name)
|
22
|
+
def define_method_attribute(attr_name, owner: nil)
|
23
23
|
generated_attribute_methods.module_eval("def #{attr_name}; @attributes['#{attr_name}']; end", __FILE__, __LINE__)
|
24
24
|
end
|
25
25
|
|
26
26
|
def type_cast(attr_name, value)
|
27
27
|
return value unless value.is_a?(String)
|
28
|
-
|
28
|
+
type_name = model_attributes[attr_name.to_sym]
|
29
|
+
type = ActiveModel::Type.lookup(type_name)
|
30
|
+
type.cast(value)
|
29
31
|
end
|
30
32
|
end
|
31
33
|
|
@@ -48,7 +50,7 @@ module TestModel
|
|
48
50
|
end
|
49
51
|
|
50
52
|
def method_missing(method_id, *args, &block)
|
51
|
-
if
|
53
|
+
if !matched_attribute_method(method_id.to_s).nil?
|
52
54
|
self.class.define_attribute_methods self.class.model_attributes.keys
|
53
55
|
send(method_id, *args, &block)
|
54
56
|
else
|
@@ -38,21 +38,6 @@ RSpec.describe ValidatesTimeliness::AttributeMethods do
|
|
38
38
|
expect(e.redefined_birth_date_called).to be_truthy
|
39
39
|
end
|
40
40
|
|
41
|
-
it 'should be undefined if model class has dynamic attribute methods reset' do
|
42
|
-
# Force method definitions
|
43
|
-
PersonWithShim.validates_date :birth_date
|
44
|
-
r = PersonWithShim.new
|
45
|
-
r.birth_date = Time.now
|
46
|
-
|
47
|
-
write_method = :birth_date=
|
48
|
-
|
49
|
-
expect(PersonWithShim.send(:generated_timeliness_methods).instance_methods).to include(write_method)
|
50
|
-
|
51
|
-
PersonWithShim.undefine_attribute_methods
|
52
|
-
|
53
|
-
expect(PersonWithShim.send(:generated_timeliness_methods).instance_methods).not_to include(write_method)
|
54
|
-
end
|
55
|
-
|
56
41
|
context "with plugin parser" do
|
57
42
|
with_config(:use_plugin_parser, true)
|
58
43
|
|