validates_timeliness 2.2.2 → 2.3.0

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.
@@ -22,24 +22,23 @@ module ValidatesTimeliness
22
22
  end
23
23
 
24
24
  def extract_date_from_multiparameter_attributes(values)
25
- year = ValidatesTimeliness::Formats.unambiguous_year(values[0].rjust(2, "0"))
26
- [year, *values.slice(1, 2).map { |s| s.rjust(2, "0") }].join("-")
25
+ year = values[0].blank? ? nil : ValidatesTimeliness::Formats.unambiguous_year(values[0].rjust(2, "0"))
26
+ [year, *values.slice(1, 2).map { |s| s.blank? ? nil : s.rjust(2, "0") }].join("-")
27
27
  end
28
28
 
29
29
  def extract_time_from_multiparameter_attributes(values)
30
- values[3..5].map { |s| s.rjust(2, "0") }.join(":")
30
+ values[3..5].map { |s| s.blank? ? nil : s.rjust(2, "0") }.join(":")
31
31
  end
32
32
 
33
33
  end
34
34
 
35
35
  module MultiparameterAttributes
36
-
36
+
37
37
  def self.included(base)
38
38
  base.alias_method_chain :execute_callstack_for_multiparameter_attributes, :timeliness
39
- end
39
+ end
40
40
 
41
41
  # Assign dates and times as formatted strings to force the use of the plugin parser
42
- # and store a before_type_cast value for attribute
43
42
  def execute_callstack_for_multiparameter_attributes_with_timeliness(callstack)
44
43
  errors = []
45
44
  callstack.each do |name, values|
@@ -47,7 +46,7 @@ module ValidatesTimeliness
47
46
  if column && [:date, :time, :datetime].include?(column.type)
48
47
  begin
49
48
  callstack.delete(name)
50
- if values.empty?
49
+ if values.empty? || values.all?(&:nil?)
51
50
  send("#{name}=", nil)
52
51
  else
53
52
  value = ValidatesTimeliness::ActiveRecord.time_array_to_string(values, column.type)
@@ -63,7 +62,7 @@ module ValidatesTimeliness
63
62
  end
64
63
  execute_callstack_for_multiparameter_attributes_without_timeliness(callstack)
65
64
  end
66
-
65
+
67
66
  end
68
67
 
69
68
  end
@@ -1,12 +1,10 @@
1
1
  require 'date'
2
2
 
3
3
  module ValidatesTimeliness
4
-
5
- # A date and time format regular expression generator. Allows you to
6
- # construct a date, time or datetime format using predefined tokens in
7
- # a string. This makes it much easier to catalogue and customize the formats
8
- # rather than dealing directly with regular expressions. The formats are then
9
- # compiled into regular expressions for use validating date or time strings.
4
+
5
+ # A date and time parsing library which allows you to add custom formats using
6
+ # simple predefined tokens. This makes it much easier to catalogue and customize
7
+ # the formats rather than dealing directly with regular expressions.
10
8
  #
11
9
  # Formats can be added or removed to customize the set of valid date or time
12
10
  # string values.
@@ -20,7 +18,7 @@ module ValidatesTimeliness
20
18
  :datetime_expressions,
21
19
  :format_tokens,
22
20
  :format_proc_args
23
-
21
+
24
22
 
25
23
  # Set the threshold value for a two digit year to be considered last century
26
24
  #
@@ -29,7 +27,7 @@ module ValidatesTimeliness
29
27
  # Example:
30
28
  # year = '29' is considered 2029
31
29
  # year = '30' is considered 1930
32
- #
30
+ #
33
31
  cattr_accessor :ambiguous_year_threshold
34
32
  self.ambiguous_year_threshold = 30
35
33
 
@@ -37,11 +35,11 @@ module ValidatesTimeliness
37
35
  # being year, month and day in that order.
38
36
  #
39
37
  # Default: [ 2000, 1, 1 ] same as ActiveRecord
40
- #
38
+ #
41
39
  cattr_accessor :dummy_date_for_time_type
42
40
  self.dummy_date_for_time_type = [ 2000, 1, 1 ]
43
41
 
44
- # Format tokens:
42
+ # Format tokens:
45
43
  # y = year
46
44
  # m = month
47
45
  # d = day
@@ -56,14 +54,14 @@ module ValidatesTimeliness
56
54
  #
57
55
  # All other characters are considered literal. You can embed regexp in the
58
56
  # format but no gurantees that it will remain intact. If you avoid the use
59
- # of any token characters and regexp dots or backslashes as special characters
60
- # in the regexp, it may well work as expected. For special characters use
57
+ # of any token characters and regexp dots or backslashes as special characters
58
+ # in the regexp, it may well work as expected. For special characters use
61
59
  # POSIX character clsses for safety.
62
60
  #
63
- # Repeating tokens:
61
+ # Repeating tokens:
64
62
  # x = 1 or 2 digits for unit (e.g. 'h' means an hour can be '9' or '09')
65
63
  # xx = 2 digits exactly for unit (e.g. 'hh' means an hour can only be '09')
66
- #
64
+ #
67
65
  # Special Cases:
68
66
  # yy = 2 or 4 digit year
69
67
  # yyyy = exactly 4 digit year
@@ -71,10 +69,10 @@ module ValidatesTimeliness
71
69
  # ddd = Day name of 3 to 9 letters (e.g. Wed or Wednesday)
72
70
  # u = microseconds matches 1 to 6 digits
73
71
  #
74
- # Any other invalid combination of repeating tokens will be swallowed up
72
+ # Any other invalid combination of repeating tokens will be swallowed up
75
73
  # by the next lowest length valid repeating token (e.g. yyy will be
76
74
  # replaced with yy)
77
-
75
+
78
76
  @@time_formats = [
79
77
  'hh:nn:ss',
80
78
  'hh-nn-ss',
@@ -88,7 +86,7 @@ module ValidatesTimeliness
88
86
  'h-nn_ampm',
89
87
  'h_ampm'
90
88
  ]
91
-
89
+
92
90
  @@date_formats = [
93
91
  'yyyy-mm-dd',
94
92
  'yyyy/mm/dd',
@@ -101,7 +99,7 @@ module ValidatesTimeliness
101
99
  'd.m.yy',
102
100
  'd mmm yy'
103
101
  ]
104
-
102
+
105
103
  @@datetime_formats = [
106
104
  'yyyy-mm-dd hh:nn:ss',
107
105
  'yyyy-mm-dd h:nn',
@@ -115,14 +113,15 @@ module ValidatesTimeliness
115
113
  'd/m/yy h:nn',
116
114
  'ddd, dd mmm yyyy hh:nn:ss (zo|tz)', # RFC 822
117
115
  'ddd mmm d hh:nn:ss zo yyyy', # Ruby time string
118
- 'yyyy-mm-ddThh:nn:ss(?:Z|zo)' # iso 8601
116
+ 'yyyy-mm-ddThh:nn:ssZ', # iso 8601 without zone offset
117
+ 'yyyy-mm-ddThh:nn:sszo' # iso 8601 with zone offset
119
118
  ]
120
-
121
-
122
- # All tokens available for format construction. The token array is made of
119
+
120
+
121
+ # All tokens available for format construction. The token array is made of
123
122
  # token regexp, validation regexp and key for format proc mapping if any.
124
123
  # If the token needs no format proc arg then the validation regexp should
125
- # not have a capturing group, as all captured groups are passed to the
124
+ # not have a capturing group, as all captured groups are passed to the
126
125
  # format proc.
127
126
  #
128
127
  # The token regexp should only use a capture group if 'look-behind' anchor
@@ -146,17 +145,17 @@ module ValidatesTimeliness
146
145
  { 'u' => [ /u{1,}/, '(\d{1,6})', :usec ] },
147
146
  { 'ampm' => [ /ampm/, '((?:[aApP])\.?[mM]\.?)', :meridian ] },
148
147
  { 'zo' => [ /zo/, '([+-]\d{2}:?\d{2})', :offset ] },
149
- { 'tz' => [ /tz/, '(?:[A-Z]{1,4})' ] },
148
+ { 'tz' => [ /tz/, '(?:[A-Z]{1,4})' ] },
150
149
  { '_' => [ /_/, '\s?' ] }
151
150
  ]
152
-
153
- # Arguments which will be passed to the format proc if matched in the
154
- # time string. The key must be the key from the format tokens. The array
155
- # consists of the arry position of the arg, the arg name, and the code to
151
+
152
+ # Arguments which will be passed to the format proc if matched in the
153
+ # time string. The key must be the key from the format tokens. The array
154
+ # consists of the arry position of the arg, the arg name, and the code to
156
155
  # place in the time array slot. The position can be nil which means the arg
157
156
  # won't be placed in the array.
158
157
  #
159
- # The code can be used to manipulate the arg value if required, otherwise
158
+ # The code can be used to manipulate the arg value if required, otherwise
160
159
  # should just be the arg name.
161
160
  #
162
161
  @@format_proc_args = {
@@ -170,15 +169,15 @@ module ValidatesTimeliness
170
169
  :offset => [7, 'z', 'offset_in_seconds(z)'],
171
170
  :meridian => [nil, 'md', nil]
172
171
  }
173
-
172
+
174
173
  class << self
175
-
174
+
176
175
  def compile_format_expressions
177
176
  @@time_expressions = compile_formats(@@time_formats)
178
177
  @@date_expressions = compile_formats(@@date_formats)
179
178
  @@datetime_expressions = compile_formats(@@datetime_formats)
180
179
  end
181
-
180
+
182
181
  # Loop through format expressions for type and call proc on matches. Allow
183
182
  # pre or post match strings to exist if strict is false. Otherwise wrap
184
183
  # regexp in start and end anchors.
@@ -206,12 +205,12 @@ module ValidatesTimeliness
206
205
  end
207
206
  last = options[:include_offset] ? 8 : 7
208
207
  if matches
209
- values = processor.call(*matches[1..last])
208
+ values = processor.call(*matches[1..last])
210
209
  values[0..2] = dummy_date_for_time_type if type == :time
211
210
  return values
212
211
  end
213
- end
214
-
212
+ end
213
+
215
214
  # Delete formats of specified type. Error raised if format not found.
216
215
  def remove_formats(type, *remove_formats)
217
216
  remove_formats.each do |format|
@@ -221,10 +220,10 @@ module ValidatesTimeliness
221
220
  end
222
221
  compile_format_expressions
223
222
  end
224
-
223
+
225
224
  # Adds new formats. Must specify format type and can specify a :before
226
- # option to nominate which format the new formats should be inserted in
227
- # front on to take higher precedence.
225
+ # option to nominate which format the new formats should be inserted in
226
+ # front on to take higher precedence.
228
227
  # Error is raised if format already exists or if :before format is not found.
229
228
  def add_formats(type, *add_formats)
230
229
  formats = self.send("#{type}_formats")
@@ -232,7 +231,7 @@ module ValidatesTimeliness
232
231
  options = add_formats.pop if add_formats.last.is_a?(Hash)
233
232
  before = options[:before]
234
233
  raise "Format for :before option #{format} was not found." if before && !formats.include?(before)
235
-
234
+
236
235
  add_formats.each do |format|
237
236
  raise "Format #{format} is already included in #{type} formats" if formats.include?(format)
238
237
 
@@ -243,7 +242,7 @@ module ValidatesTimeliness
243
242
  end
244
243
 
245
244
  # Removes formats where the 1 or 2 digit month comes first, to eliminate
246
- # formats which are ambiguous with the European style of day then month.
245
+ # formats which are ambiguous with the European style of day then month.
247
246
  # The mmm token is ignored as its not ambigous.
248
247
  def remove_us_formats
249
248
  us_format_regexp = /\Am{1,2}[^m]/
@@ -251,7 +250,7 @@ module ValidatesTimeliness
251
250
  datetime_formats.reject! { |format| us_format_regexp =~ format }
252
251
  compile_format_expressions
253
252
  end
254
-
253
+
255
254
  def full_hour(hour, meridian)
256
255
  hour = hour.to_i
257
256
  return hour if meridian.nil?
@@ -296,18 +295,18 @@ module ValidatesTimeliness
296
295
  end
297
296
 
298
297
  private
299
-
300
- # Compile formats into validation regexps and format procs
301
- def format_expression_generator(string_format)
302
- regexp = string_format.dup
298
+
299
+ # Generate regular expression and processor from format string
300
+ def generate_format_expression(string_format)
301
+ regexp = string_format.dup
303
302
  order = {}
304
303
  regexp.gsub!(/([\.\\])/, '\\\\\1') # escapes dots and backslashes
305
-
304
+
306
305
  format_tokens.each do |token|
307
306
  token_name = token.keys.first
308
307
  token_regexp, regexp_str, arg_key = *token.values.first
309
-
310
- # hack for lack of look-behinds. If has a capture group then is
308
+
309
+ # hack for lack of look-behinds. If has a capture group then is
311
310
  # considered an anchor to put straight back in the regexp string.
312
311
  regexp.gsub!(token_regexp) {|m| "#{$1}" + regexp_str }
313
312
  order[arg_key] = $~.begin(0) if $~ && !arg_key.nil?
@@ -317,8 +316,8 @@ module ValidatesTimeliness
317
316
  rescue
318
317
  raise "The following format regular expression failed to compile: #{regexp}\n from format #{string_format}."
319
318
  end
320
-
321
- # Generates a proc which when executed maps the regexp capture groups to a
319
+
320
+ # Generates a proc which when executed maps the regexp capture groups to a
322
321
  # proc argument based on order captured. A time array is built using the proc
323
322
  # argument in the position indicated by the first element of the proc arg
324
323
  # array.
@@ -329,19 +328,19 @@ module ValidatesTimeliness
329
328
  arr = [nil] * 7
330
329
  order.keys.each {|k| i = arg_map[k][0]; arr[i] = arg_map[k][2] unless i.nil? }
331
330
  proc_string = <<-EOL
332
- lambda {|#{args.join(',')}|
331
+ lambda {|#{args.join(',')}|
333
332
  md ||= nil
334
333
  [#{arr.map {|i| i.nil? ? 'nil' : i }.join(',')}].map {|i| i.is_a?(Float) ? i : i.to_i }
335
334
  }
336
335
  EOL
337
336
  eval proc_string
338
337
  end
339
-
338
+
340
339
  def compile_formats(formats)
341
- formats.map { |format| [ format, *format_expression_generator(format) ] }
340
+ formats.map { |format| [ format, *generate_format_expression(format) ] }
342
341
  end
343
-
344
- # Pick expression set and combine date and datetimes for
342
+
343
+ # Pick expression set and combine date and datetimes for
345
344
  # datetime attributes to allow date string as datetime
346
345
  def expression_set(type, string)
347
346
  case type
@@ -358,7 +357,7 @@ module ValidatesTimeliness
358
357
  end
359
358
  end
360
359
  end
361
-
360
+
362
361
  end
363
362
  end
364
363
  end
@@ -5,7 +5,7 @@ en:
5
5
  invalid_date: "is not a valid date"
6
6
  invalid_time: "is not a valid time"
7
7
  invalid_datetime: "is not a valid datetime"
8
- equal_to: "must be equal to {{restriction}}"
8
+ is_at: "must be at {{restriction}}"
9
9
  before: "must be before {{restriction}}"
10
10
  on_or_before: "must be on or before {{restriction}}"
11
11
  after: "must be after {{restriction}}"
@@ -2,7 +2,7 @@ module Spec
2
2
  module Rails
3
3
  module Matchers
4
4
  class ValidateTimeliness
5
-
5
+
6
6
  VALIDITY_TEST_VALUES = {
7
7
  :date => {:pass => '2000-01-01', :fail => '2000-01-32'},
8
8
  :time => {:pass => '12:00', :fail => '25:00'},
@@ -10,7 +10,7 @@ module Spec
10
10
  }
11
11
 
12
12
  OPTION_TEST_SETTINGS = {
13
- :equal_to => { :method => :+, :modify_on => :invalid },
13
+ :is_at => { :method => :+, :modify_on => :invalid },
14
14
  :before => { :method => :-, :modify_on => :valid },
15
15
  :after => { :method => :+, :modify_on => :valid },
16
16
  :on_or_before => { :method => :+, :modify_on => :invalid },
@@ -25,10 +25,10 @@ module Spec
25
25
  def matches?(record)
26
26
  @record = record
27
27
  @type = @options[:type]
28
-
28
+
29
29
  valid = test_validity
30
30
 
31
- valid = test_option(:equal_to) if valid && @options[:equal_to]
31
+ valid = test_option(:is_at) if valid && @options[:is_at]
32
32
  valid = test_option(:before) if valid && @options[:before]
33
33
  valid = test_option(:after) if valid && @options[:after]
34
34
  valid = test_option(:on_or_before) if valid && @options[:on_or_before]
@@ -37,21 +37,21 @@ module Spec
37
37
 
38
38
  return valid
39
39
  end
40
-
40
+
41
41
  def failure_message
42
42
  "expected model to validate #{@type} attribute #{@expected.inspect} with #{@last_failure}"
43
43
  end
44
-
44
+
45
45
  def negative_failure_message
46
46
  "expected not to validate #{@type} attribute #{@expected.inspect}"
47
47
  end
48
-
48
+
49
49
  def description
50
50
  "have validated #{@type} attribute #{@expected.inspect}"
51
51
  end
52
-
52
+
53
53
  private
54
-
54
+
55
55
  def test_validity
56
56
  invalid_value = VALIDITY_TEST_VALUES[@type][:fail]
57
57
  valid_value = parse_and_cast(VALIDITY_TEST_VALUES[@type][:pass])
@@ -62,7 +62,7 @@ module Spec
62
62
  def test_option(option)
63
63
  settings = OPTION_TEST_SETTINGS[option]
64
64
  boundary = parse_and_cast(@options[option])
65
-
65
+
66
66
  method = settings[:method]
67
67
 
68
68
  valid_value, invalid_value = if settings[:modify_on] == :valid
@@ -70,27 +70,27 @@ module Spec
70
70
  else
71
71
  [ boundary, boundary.send(method, 1) ]
72
72
  end
73
-
74
- error_matching(invalid_value, option) &&
73
+
74
+ error_matching(invalid_value, option) &&
75
75
  no_error_matching(valid_value, option)
76
76
  end
77
77
 
78
78
  def test_before
79
79
  before = parse_and_cast(@options[:before])
80
80
 
81
- error_matching(before - 1, :before) &&
81
+ error_matching(before - 1, :before) &&
82
82
  no_error_matching(before, :before)
83
83
  end
84
84
 
85
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) &&
86
+ between = parse_and_cast(@options[:between])
87
+
88
+ error_matching(between.first - 1, :between) &&
89
+ error_matching(between.last + 1, :between) &&
90
90
  no_error_matching(between.first, :between) &&
91
91
  no_error_matching(between.last, :between)
92
92
  end
93
-
93
+
94
94
  def parse_and_cast(value)
95
95
  value = @validator.class.send(:evaluate_option_value, value, @type, @record)
96
96
  @validator.class.send(:type_cast_value, value, @type)
@@ -105,7 +105,7 @@ module Spec
105
105
  @last_failure = "error matching '#{match}' when value is #{format_value(value)}" unless pass
106
106
  pass
107
107
  end
108
-
108
+
109
109
  def no_error_matching(value, option)
110
110
  pass = !error_matching(value, option)
111
111
  unless pass
@@ -115,29 +115,28 @@ module Spec
115
115
  pass
116
116
  end
117
117
 
118
- def error_message_for(option)
119
- msg = @validator.error_messages[option]
120
- restriction = @validator.class.send(:evaluate_option_value, @validator.configuration[option], @type, @record)
121
-
122
- if restriction
123
- restriction = [restriction] unless restriction.is_a?(Array)
124
- restriction.map! {|r| @validator.class.send(:type_cast_value, r, @type) }
125
- interpolate = @validator.send(:interpolation_values, option, restriction )
126
-
127
- # get I18n message if defined and has interpolation keys in msg
128
- if defined?(I18n) && !@validator.send(:custom_error_messages).include?(option)
129
- msg = if defined?(ActiveRecord::Error)
130
- ActiveRecord::Error.new(@record, @expected, option, interpolate).message
131
- else
132
- @record.errors.generate_message(@expected, option, interpolate)
133
- end
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
134
131
  else
135
- msg = msg % interpolate
132
+ @record.errors.generate_message(@expected, message, options)
136
133
  end
137
- end
138
- msg
134
+ else
135
+ interpolate ||= nil
136
+ @validator.error_messages[message] % interpolate
137
+ end
139
138
  end
140
-
139
+
141
140
  def format_value(value)
142
141
  return value if value.is_a?(String)
143
142
  value.strftime(@validator.class.error_value_formats[@type])