validates_timeliness 2.2.2 → 2.3.0

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