validates_timeliness 1.1.7 → 2.0.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.
data/CHANGELOG CHANGED
@@ -1,3 +1,11 @@
1
+ = 2.0.0 [2009-04-12]
2
+ - Error value formats are now specified in the i18n locale file instead of updating plugin hash. See OTHER CUSTOMISATION section in README.
3
+ - Date/time select helper extension is disabled by default. To enable see DISPLAY INVALID VALUES IN DATE HELPERS section in README to enable.
4
+ - Added :format option to limit validation to a single format if desired
5
+ - Matcher now supports :equal_to option
6
+ - Formats.parse can take :include_offset option to include offset value from string in seconds, if string contains an offset. Offset not used in rest of plugin yet.
7
+ - Refactored to remove as much plugin code from ActiveRecord as possible.
8
+
1
9
  = 1.1.7 [2009-03-26]
2
10
  - Minor change to multiparameter attributes which I had not properly implemented for chaining
3
11
 
data/README.rdoc CHANGED
@@ -80,6 +80,7 @@ Special options:
80
80
  :with_time - Validate a date attribute value combined with a time value against any temporal restrictions
81
81
  :with_date - Validate a time attribute value combined with a date value against any temporal restrictions
82
82
  :ignore_usec - Ignores microsecond value on datetime restrictions
83
+ :format - Limit validation to a single format for special cases. Takes plugin format value.
83
84
 
84
85
  Message options: - Use these to override the default error messages
85
86
  :invalid_date_message
@@ -266,6 +267,20 @@ corner cases a little harder to test. In general if you are using procs or
266
267
  model methods and you only care when they return a value, then they should
267
268
  return nil in all other situations. Restrictions are skipped if they are nil.
268
269
 
270
+
271
+ === DISPLAY INVALID VALUES IN DATE HELPERS:
272
+
273
+ The plugin has some extensions to ActionView and ActiveRecord by allowing invalid
274
+ date and time values to be redisplayed to the user as feedback, instead of
275
+ a blank field which happens by default in Rails. Though the date helpers make this a
276
+ pretty rare occurence, given the select dropdowns for each date/time component, but
277
+ it may be something of interest.
278
+
279
+ To activate it, put this in an initializer:
280
+
281
+ ValidatesTimeliness.enable_datetime_select_extension!
282
+
283
+
269
284
  === OTHER CUSTOMISATION:
270
285
 
271
286
  The error messages for each temporal restrictions can also be globally overridden by
@@ -302,12 +317,22 @@ will be inserted.
302
317
  And for something a little more specific you can override the format of the interpolation
303
318
  values inserted in the error messages for temporal restrictions like so
304
319
 
320
+ For Rails 2.0/2.1:
321
+
305
322
  ValidatesTimeliness::Validator.error_value_formats.update(
306
323
  :time => '%H:%M:%S',
307
324
  :date => '%Y-%m-%d',
308
325
  :datetime => '%Y-%m-%d %H:%M:%S'
309
326
  )
310
327
 
328
+ Rails 2.2+ using the I18n system to define new defaults:
329
+
330
+ validates_timeliness:
331
+ error_value_formats:
332
+ date: '%Y-%m-%d'
333
+ time: '%H:%M:%S'
334
+ datetime: '%Y-%m-%d %H:%M:%S'
335
+
311
336
  Those are Ruby strftime formats not the plugin formats.
312
337
 
313
338
 
data/Rakefile CHANGED
@@ -5,7 +5,7 @@ require 'date'
5
5
  require 'spec/rake/spectask'
6
6
 
7
7
  GEM = "validates_timeliness"
8
- GEM_VERSION = "1.1.7"
8
+ GEM_VERSION = "2.0.0"
9
9
  AUTHOR = "Adam Meehan"
10
10
  EMAIL = "adam.meehan@gmail.com"
11
11
  HOMEPAGE = "http://github.com/adzap/validates_timeliness"
data/TODO CHANGED
@@ -1,7 +1,5 @@
1
- - :format option
2
1
  - valid formats could come from locale file
3
2
  - add replace_formats instead add_formats :before
4
- - array of values to all temporal options
5
- - use tz value from time string?
6
- - move make_time out of AR
7
-
3
+ - array of values for all temporal options
4
+ - use tz and zo value from time string?
5
+ - filter valid formats rather than remove for hot swapping without recompilation
@@ -37,7 +37,7 @@ module ValidatesTimeliness
37
37
  return value_without_timeliness(object)
38
38
  end
39
39
 
40
- time_array = ParseDate.parsedate(raw_value)
40
+ time_array = ValidatesTimeliness::Formats.parse(raw_value, :datetime)
41
41
 
42
42
  TimelinessDateTime.new(*time_array[0..5])
43
43
  end
@@ -46,7 +46,7 @@ module ValidatesTimeliness
46
46
  # implementation as it chains the write_attribute method which deletes
47
47
  # the attribute from the cache.
48
48
  def write_date_time_attribute(attr_name, value, type, time_zone_aware)
49
- new = self.class.parse_date_time(value, type)
49
+ new = ValidatesTimeliness::Parser.parse(value, type)
50
50
 
51
51
  if new && type != :date
52
52
  new = new.to_time
@@ -73,18 +73,16 @@ module ValidatesTimeliness
73
73
 
74
74
  if @attributes_cache.has_key?(attr_name)
75
75
  time = read_attribute_before_type_cast(attr_name)
76
- time = self.class.parse_date_time(time, type)
76
+ time = ValidatesTimeliness::Parser.parse(time, type)
77
77
  else
78
78
  time = read_attribute(attr_name)
79
- @attributes[attr_name] = time && time_zone_aware ? time.in_time_zone : time
79
+ @attributes[attr_name] = (time && time_zone_aware ? time.in_time_zone : time) unless frozen?
80
80
  end
81
81
  @attributes_cache[attr_name] = time && time_zone_aware ? time.in_time_zone : time
82
82
  end
83
83
 
84
84
  module ClassMethods
85
85
 
86
- # Define attribute reader and writer method for date, time and
87
- # datetime attributes to use plugin parser.
88
86
  def define_attribute_methods_with_timeliness
89
87
  return if generated_methods?
90
88
  columns_hash.each do |name, column|
@@ -105,7 +103,6 @@ module ValidatesTimeliness
105
103
  define_attribute_methods_without_timeliness
106
104
  end
107
105
 
108
- # Define write method for date, time and datetime columns
109
106
  def define_write_method_for_dates_and_times(attr_name, type, time_zone_aware)
110
107
  method_body = <<-EOV
111
108
  def #{attr_name}=(value)
@@ -38,7 +38,7 @@ module ValidatesTimeliness
38
38
  end
39
39
 
40
40
  def time_array_to_string(values, type)
41
- values = values.map {|v| v.to_s }
41
+ values.collect! {|v| v.to_s }
42
42
 
43
43
  case type
44
44
  when :date
@@ -124,13 +124,13 @@ module ValidatesTimeliness
124
124
  { 's' => [ /s{1}/, '(\d{1,2})', :sec ] },
125
125
  { 'u' => [ /u{1,}/, '(\d{1,6})', :usec ] },
126
126
  { 'ampm' => [ /ampm/, '((?:[aApP])\.?[mM]\.?)', :meridian ] },
127
- { 'zo' => [ /zo/, '(?:[+-]\d{2}:?\d{2})'] },
127
+ { 'zo' => [ /zo/, '([+-]\d{2}:?\d{2})', :offset ] },
128
128
  { 'tz' => [ /tz/, '(?:[A-Z]{1,4})' ] },
129
129
  { '_' => [ /_/, '\s?' ] }
130
130
  ]
131
131
 
132
- # Arguments whichs will be passed to the format proc if matched in the
133
- # time string. The key must should the key from the format tokens. The array
132
+ # Arguments which will be passed to the format proc if matched in the
133
+ # time string. The key must be the key from the format tokens. The array
134
134
  # consists of the arry position of the arg, the arg name, and the code to
135
135
  # place in the time array slot. The position can be nil which means the arg
136
136
  # won't be placed in the array.
@@ -146,6 +146,7 @@ module ValidatesTimeliness
146
146
  :min => [4, 'n', 'n'],
147
147
  :sec => [5, 's', 's'],
148
148
  :usec => [6, 'u', 'microseconds(u)'],
149
+ :offset => [7, 'z', 'offset_in_seconds(z)'],
149
150
  :meridian => [nil, 'md', nil]
150
151
  }
151
152
 
@@ -160,21 +161,29 @@ module ValidatesTimeliness
160
161
  # Loop through format expressions for type and call proc on matches. Allow
161
162
  # pre or post match strings to exist if strict is false. Otherwise wrap
162
163
  # regexp in start and end anchors.
163
- # Returns 7 part time array.
164
- def parse(string, type, strict=true)
164
+ # Returns time array if matches a format, nil otherwise.
165
+ def parse(string, type, options={})
165
166
  return string unless string.is_a?(String)
167
+ options.reverse_merge!(:strict => true)
168
+
169
+ sets = if options[:format]
170
+ [ send("#{type}_expressions").assoc(options[:format]) ]
171
+ else
172
+ expression_set(type, string)
173
+ end
166
174
 
167
175
  matches = nil
168
- exp, processor = expression_set(type, string).find do |regexp, proc|
169
- full = /\A#{regexp}\Z/ if strict
176
+ processor = sets.each do |format, regexp, proc|
177
+ full = /\A#{regexp}\Z/ if options[:strict]
170
178
  full ||= case type
171
179
  when :date then /\A#{regexp}/
172
180
  when :time then /#{regexp}\Z/
173
181
  when :datetime then /\A#{regexp}\Z/
174
182
  end
175
- matches = full.match(string.strip)
183
+ break(proc) if matches = full.match(string.strip)
176
184
  end
177
- processor.call(*matches[1..7]) if matches
185
+ last = options[:include_offset] ? 8 : 7
186
+ processor.call(*matches[1..last]) if matches
178
187
  end
179
188
 
180
189
  # Delete formats of specified type. Error raised if format not found.
@@ -206,8 +215,7 @@ module ValidatesTimeliness
206
215
  end
207
216
  compile_format_expressions
208
217
  end
209
-
210
-
218
+
211
219
  # Removes formats where the 1 or 2 digit month comes first, to eliminate
212
220
  # formats which are ambiguous with the European style of day then month.
213
221
  # The mmm token is ignored as its not ambigous.
@@ -246,22 +254,17 @@ module ValidatesTimeliness
246
254
  # argument in the position indicated by the first element of the proc arg
247
255
  # array.
248
256
  #
249
- # Examples:
250
- #
251
- # 'yyyy-mm-dd hh:nn' => lambda {|y,m,d,h,n| md||=0; [unambiguous_year(y),month_index(m),d,full_hour(h,md),n,nil,nil].map {|i| i.to_i } }
252
- # 'dd/mm/yyyy h:nn_ampm' => lambda {|d,m,y,h,n,md| md||=0; [unambiguous_year(y),month_index(m),d,full_hour(h,md),n,nil,nil].map {|i| i.to_i } }
253
- #
254
257
  def format_proc(order)
255
258
  arg_map = format_proc_args
256
259
  args = order.invert.sort.map {|p| arg_map[p[1]][1] }
257
260
  arr = [nil] * 7
258
261
  order.keys.each {|k| i = arg_map[k][0]; arr[i] = arg_map[k][2] unless i.nil? }
259
- proc_string = "lambda {|#{args.join(',')}| md||=nil; [#{arr.map {|i| i.nil? ? 'nil' : i }.join(',')}].map {|i| i.to_i } }"
262
+ proc_string = "lambda {|#{args.join(',')}| md||=nil; [#{arr.map {|i| i.nil? ? 'nil' : i }.join(',')}].map {|i| i.is_a?(Float) ? i : i.to_i } }"
260
263
  eval proc_string
261
264
  end
262
265
 
263
266
  def compile_formats(formats)
264
- formats.map { |format| regexp, format_proc = format_expression_generator(format) }
267
+ formats.map { |format| [ format, *format_expression_generator(format) ] }
265
268
  end
266
269
 
267
270
  # Pick expression set and combine date and datetimes for
@@ -313,6 +316,13 @@ module ValidatesTimeliness
313
316
  def microseconds(usec)
314
317
  (".#{usec}".to_f * 1_000_000).to_i
315
318
  end
319
+
320
+ def offset_in_seconds(offset)
321
+ sign = offset =~ /^-/ ? -1 : 1
322
+ parts = offset.scan(/\d\d/).map {|p| p.to_f }
323
+ parts[1] = parts[1].to_f / 60
324
+ (parts[0] + parts[1]) * sign * 3600
325
+ end
316
326
  end
317
327
  end
318
328
  end
@@ -11,3 +11,8 @@ en:
11
11
  after: "must be after {{restriction}}"
12
12
  on_or_after: "must be on or after {{restriction}}"
13
13
  between: "must be between {{earliest}} and {{latest}}"
14
+ validates_timeliness:
15
+ error_value_formats:
16
+ date: '%Y-%m-%d'
17
+ time: '%H:%M:%S'
18
+ datetime: '%Y-%m-%d %H:%M:%S'
@@ -0,0 +1,46 @@
1
+ module ValidatesTimeliness
2
+ module Parser
3
+
4
+ class << self
5
+
6
+ def parse(raw_value, type, options={})
7
+ return nil if raw_value.blank?
8
+ return raw_value if raw_value.acts_like?(:time) || raw_value.is_a?(Date)
9
+
10
+ options.reverse_merge!(:strict => true)
11
+
12
+ time_array = ValidatesTimeliness::Formats.parse(raw_value, type, options)
13
+ raise if time_array.nil?
14
+
15
+ # Rails dummy time date part is defined as 2000-01-01
16
+ time_array[0..2] = 2000, 1, 1 if type == :time
17
+
18
+ # Date.new enforces days per month, unlike Time
19
+ date = Date.new(*time_array[0..2]) unless type == :time
20
+
21
+ return date if type == :date
22
+
23
+ make_time(time_array[0..7])
24
+ rescue
25
+ nil
26
+ end
27
+
28
+ def make_time(time_array)
29
+ if Time.respond_to?(:zone) && ValidatesTimeliness.use_time_zones
30
+ Time.zone.local(*time_array)
31
+ else
32
+ begin
33
+ time_zone = ValidatesTimeliness.default_timezone
34
+ Time.send(time_zone, *time_array)
35
+ rescue ArgumentError, TypeError
36
+ zone_offset = time_zone == :local ? DateTime.local_offset : 0
37
+ time_array.pop # remove microseconds
38
+ DateTime.civil(*(time_array << zone_offset))
39
+ end
40
+ end
41
+ end
42
+
43
+ end
44
+
45
+ end
46
+ end
@@ -10,6 +10,7 @@ module Spec
10
10
  }
11
11
 
12
12
  OPTION_TEST_SETTINGS = {
13
+ :equal_to => { :method => :+, :modify_on => :invalid },
13
14
  :before => { :method => :-, :modify_on => :valid },
14
15
  :after => { :method => :+, :modify_on => :valid },
15
16
  :on_or_before => { :method => :+, :modify_on => :invalid },
@@ -27,9 +28,9 @@ module Spec
27
28
 
28
29
  valid = test_validity
29
30
 
31
+ valid = test_option(:equal_to) if @options[:equal_to] && valid
30
32
  valid = test_option(:before) if @options[:before] && valid
31
33
  valid = test_option(:after) if @options[:after] && valid
32
-
33
34
  valid = test_option(:on_or_before) if @options[:on_or_before] && valid
34
35
  valid = test_option(:on_or_after) if @options[:on_or_after] && valid
35
36
 
@@ -116,7 +117,7 @@ module Spec
116
117
  end
117
118
 
118
119
  def error_message_for(option)
119
- msg = @validator.send(:error_messages)[option]
120
+ msg = @validator.error_messages[option]
120
121
  restriction = @validator.class.send(:evaluate_option_value, @validator.configuration[option], @type, @record)
121
122
 
122
123
  if restriction
@@ -135,7 +136,7 @@ module Spec
135
136
 
136
137
  def format_value(value)
137
138
  return value if value.is_a?(String)
138
- value.strftime(ValidatesTimeliness::Validator.error_value_formats[@type])
139
+ value.strftime(@validator.class.error_value_formats[@type])
139
140
  end
140
141
  end
141
142
 
@@ -7,27 +7,6 @@ module ValidatesTimeliness
7
7
 
8
8
  module ClassMethods
9
9
 
10
- def parse_date_time(raw_value, type, strict=true)
11
- return nil if raw_value.blank?
12
- return raw_value if raw_value.acts_like?(:time) || raw_value.is_a?(Date)
13
-
14
- time_array = ValidatesTimeliness::Formats.parse(raw_value, type, strict)
15
- raise if time_array.nil?
16
-
17
- # Rails dummy time date part is defined as 2000-01-01
18
- time_array[0..2] = 2000, 1, 1 if type == :time
19
-
20
- # Date.new enforces days per month, unlike Time
21
- date = Date.new(*time_array[0..2]) unless type == :time
22
-
23
- return date if type == :date
24
-
25
- # Create time object which checks time part, and return time object
26
- make_time(time_array)
27
- rescue
28
- nil
29
- end
30
-
31
10
  def validates_time(*attr_names)
32
11
  configuration = attr_names.extract_options!
33
12
  configuration[:type] = :time
@@ -59,21 +38,6 @@ module ValidatesTimeliness
59
38
  end
60
39
  end
61
40
 
62
- # Time.zone. Rails 2.0 should be default_timezone.
63
- def make_time(time_array)
64
- if Time.respond_to?(:zone) && time_zone_aware_attributes
65
- Time.zone.local(*time_array)
66
- else
67
- begin
68
- Time.send(::ActiveRecord::Base.default_timezone, *time_array)
69
- rescue ArgumentError, TypeError
70
- zone_offset = ::ActiveRecord::Base.default_timezone == :local ? DateTime.local_offset : 0
71
- time_array.pop # remove microseconds
72
- DateTime.civil(*(time_array << zone_offset))
73
- end
74
- end
75
- end
76
-
77
41
  end
78
42
 
79
43
  end
@@ -2,14 +2,7 @@ module ValidatesTimeliness
2
2
 
3
3
  class Validator
4
4
  cattr_accessor :ignore_restriction_errors
5
- cattr_accessor :error_value_formats
6
-
7
5
  self.ignore_restriction_errors = false
8
- self.error_value_formats = {
9
- :time => '%H:%M:%S',
10
- :date => '%Y-%m-%d',
11
- :datetime => '%Y-%m-%d %H:%M:%S'
12
- }
13
6
 
14
7
  RESTRICTION_METHODS = {
15
8
  :equal_to => :==,
@@ -21,8 +14,8 @@ module ValidatesTimeliness
21
14
  }
22
15
 
23
16
  VALID_OPTIONS = [
24
- :on, :if, :unless, :allow_nil, :empty, :allow_blank, :blank,
25
- :with_time, :with_date, :ignore_usec,
17
+ :on, :if, :unless, :allow_nil, :empty, :allow_blank,
18
+ :with_time, :with_date, :ignore_usec, :format,
26
19
  :invalid_time_message, :invalid_date_message, :invalid_datetime_message
27
20
  ] + RESTRICTION_METHODS.keys.map {|option| [option, "#{option}_message".to_sym] }.flatten
28
21
 
@@ -36,18 +29,32 @@ module ValidatesTimeliness
36
29
  end
37
30
 
38
31
  def call(record, attr_name, value)
39
- value = record.class.parse_date_time(value, type, false) if value.is_a?(String)
40
32
  raw_value = raw_value(record, attr_name) || value
41
33
 
34
+ if value.is_a?(String) || configuration[:format]
35
+ strict = !configuration[:format].nil?
36
+ value = ValidatesTimeliness::Parser.parse(raw_value, type, :strict => strict, :format => configuration[:format])
37
+ end
38
+
42
39
  return if (raw_value.nil? && configuration[:allow_nil]) || (raw_value.blank? && configuration[:allow_blank])
43
40
 
44
- add_error(record, attr_name, :blank) and return if raw_value.blank?
45
-
46
- add_error(record, attr_name, "invalid_#{type}".to_sym) and return unless value
41
+ if raw_value.blank?
42
+ add_error(record, attr_name, :blank)
43
+ return
44
+ end
45
+
46
+ if value.nil?
47
+ add_error(record, attr_name, "invalid_#{type}".to_sym)
48
+ return
49
+ end
47
50
 
48
51
  validate_restrictions(record, attr_name, value)
49
52
  end
50
-
53
+
54
+ def error_messages
55
+ @error_messages ||= self.class.default_error_messages.merge(custom_error_messages)
56
+ end
57
+
51
58
  private
52
59
 
53
60
  def raw_value(record, attr_name)
@@ -55,12 +62,12 @@ module ValidatesTimeliness
55
62
  end
56
63
 
57
64
  def validate_restrictions(record, attr_name, value)
58
- value = if @configuration[:with_time] || @configuration[:with_date]
65
+ value = if configuration[:with_time] || configuration[:with_date]
59
66
  restriction_type = :datetime
60
67
  combine_date_and_time(value, record)
61
68
  else
62
69
  restriction_type = type
63
- self.class.type_cast_value(value, type, @configuration[:ignore_usec])
70
+ self.class.type_cast_value(value, type, configuration[:ignore_usec])
64
71
  end
65
72
  return if value.nil?
66
73
 
@@ -69,7 +76,7 @@ module ValidatesTimeliness
69
76
  begin
70
77
  restriction = self.class.evaluate_option_value(restriction, restriction_type, record)
71
78
  next if restriction.nil?
72
- restriction = self.class.type_cast_value(restriction, restriction_type, @configuration[:ignore_usec])
79
+ restriction = self.class.type_cast_value(restriction, restriction_type, configuration[:ignore_usec])
73
80
 
74
81
  unless evaluate_restriction(restriction, value, method)
75
82
  add_error(record, attr_name, option, interpolation_values(option, restriction))
@@ -87,7 +94,7 @@ module ValidatesTimeliness
87
94
  restriction = [restriction] unless restriction.is_a?(Array)
88
95
 
89
96
  if defined?(I18n)
90
- message = custom_error_messages[option] || I18n.translate('activerecord.errors.messages')[option]
97
+ message = custom_error_messages[option] || I18n.t('activerecord.errors.messages')[option]
91
98
  subs = message.scan(/\{\{([^\}]*)\}\}/)
92
99
  interpolations = {}
93
100
  subs.each_with_index {|s, i| interpolations[s[0].to_sym] = restriction[i].strftime(format) }
@@ -110,7 +117,6 @@ module ValidatesTimeliness
110
117
 
111
118
  def add_error(record, attr_name, message, interpolate=nil)
112
119
  if defined?(I18n)
113
- # use i18n support in AR for message or use custom message passed to validation method
114
120
  custom = custom_error_messages[message]
115
121
  record.errors.add(attr_name, custom || message, interpolate || {})
116
122
  else
@@ -120,10 +126,6 @@ module ValidatesTimeliness
120
126
  end
121
127
  end
122
128
 
123
- def error_messages
124
- @error_messages ||= ValidatesTimeliness.default_error_messages.merge(custom_error_messages)
125
- end
126
-
127
129
  def custom_error_messages
128
130
  @custom_error_messages ||= configuration.inject({}) {|msgs, (k, v)|
129
131
  if md = /(.*)_message$/.match(k.to_s)
@@ -132,33 +134,53 @@ module ValidatesTimeliness
132
134
  msgs
133
135
  }
134
136
  end
135
-
137
+
136
138
  def combine_date_and_time(value, record)
137
139
  if type == :date
138
140
  date = value
139
- time = @configuration[:with_time]
141
+ time = configuration[:with_time]
140
142
  else
141
- date = @configuration[:with_date]
143
+ date = configuration[:with_date]
142
144
  time = value
143
145
  end
144
146
  date, time = self.class.evaluate_option_value(date, :date, record), self.class.evaluate_option_value(time, :time, record)
145
147
  return if date.nil? || time.nil?
146
- record.class.send(:make_time, [date.year, date.month, date.day, time.hour, time.min, time.sec, time.usec])
148
+ ValidatesTimeliness::Parser.make_time([date.year, date.month, date.day, time.hour, time.min, time.sec, time.usec])
147
149
  end
148
150
 
149
151
  def validate_options(options)
150
- invalid_for_type = ([:time, :date, :datetime] - [@type]).map {|k| "invalid_#{k}_message".to_sym }
151
- invalid_for_type << :with_date unless @type == :time
152
- invalid_for_type << :with_time unless @type == :date
152
+ invalid_for_type = ([:time, :date, :datetime] - [type]).map {|k| "invalid_#{k}_message".to_sym }
153
+ invalid_for_type << :with_date unless type == :time
154
+ invalid_for_type << :with_time unless type == :date
153
155
  options.assert_valid_keys(VALID_OPTIONS - invalid_for_type)
154
156
  end
155
157
 
156
158
  # class methods
157
159
  class << self
158
160
 
161
+ def default_error_messages
162
+ if defined?(I18n)
163
+ I18n.t('activerecord.errors.messages')
164
+ else
165
+ ::ActiveRecord::Errors.default_error_messages
166
+ end
167
+ end
168
+
169
+ def error_value_formats
170
+ if defined?(I18n)
171
+ I18n.t('validates_timeliness.error_value_formats')
172
+ else
173
+ @@error_value_formats
174
+ end
175
+ end
176
+
177
+ def error_value_formats=(formats)
178
+ @@error_value_formats = formats
179
+ end
180
+
159
181
  def evaluate_option_value(value, type, record)
160
182
  case value
161
- when Time, Date, DateTime
183
+ when Time, Date
162
184
  value
163
185
  when Symbol
164
186
  evaluate_option_value(record.send(value), type, record)
@@ -169,7 +191,7 @@ module ValidatesTimeliness
169
191
  when Range
170
192
  evaluate_option_value([value.first, value.last], type, record)
171
193
  else
172
- record.class.parse_date_time(value, type, false)
194
+ ValidatesTimeliness::Parser.parse(value, type, :strict => false)
173
195
  end
174
196
  end
175
197
 
@@ -192,7 +214,7 @@ module ValidatesTimeliness
192
214
  nil
193
215
  end
194
216
  if ignore_usec && value.is_a?(Time)
195
- ::ActiveRecord::Base.send(:make_time, Array(value).reverse[4..9])
217
+ ValidatesTimeliness::Parser.make_time(Array(value).reverse[4..9])
196
218
  else
197
219
  value
198
220
  end
@@ -1,4 +1,5 @@
1
1
  require 'validates_timeliness/formats'
2
+ require 'validates_timeliness/parser'
2
3
  require 'validates_timeliness/validator'
3
4
  require 'validates_timeliness/validation_methods'
4
5
  require 'validates_timeliness/spec/rails/matchers/validate_timeliness' if ENV['RAILS_ENV'] == 'test'
@@ -14,9 +15,11 @@ require 'validates_timeliness/core_ext/date_time'
14
15
  module ValidatesTimeliness
15
16
 
16
17
  mattr_accessor :default_timezone
17
-
18
18
  self.default_timezone = :utc
19
19
 
20
+ mattr_accessor :use_time_zones
21
+ self.use_time_zones = false
22
+
20
23
  LOCALE_PATH = File.expand_path(File.dirname(__FILE__) + '/validates_timeliness/locale/en.yml')
21
24
 
22
25
  class << self
@@ -31,25 +34,18 @@ module ValidatesTimeliness
31
34
  I18n.load_path += [ LOCALE_PATH ]
32
35
  I18n.reload!
33
36
  else
34
- messages = YAML::load(IO.read(LOCALE_PATH))
35
- errors = messages['en']['activerecord']['errors']['messages'].inject({}) {|h,(k,v)| h[k.to_sym] = v.gsub(/\{\{\w*\}\}/, '%s');h }
37
+ defaults = YAML::load(IO.read(LOCALE_PATH))['en']
38
+ errors = defaults['activerecord']['errors']['messages'].inject({}) {|h,(k,v)| h[k.to_sym] = v.gsub(/\{\{\w*\}\}/, '%s');h }
36
39
  ::ActiveRecord::Errors.default_error_messages.update(errors)
40
+
41
+ ValidatesTimeliness::Validator.error_value_formats = defaults['validates_timeliness']['error_value_formats'].symbolize_keys
37
42
  end
38
43
  end
39
44
 
40
- def default_error_messages
41
- if Rails::VERSION::STRING < '2.2'
42
- ::ActiveRecord::Errors.default_error_messages
43
- else
44
- I18n.translate('activerecord.errors.messages')
45
- end
46
- end
47
-
48
45
  def setup_for_rails
49
- major, minor = Rails::VERSION::MAJOR, Rails::VERSION::MINOR
50
46
  self.default_timezone = ::ActiveRecord::Base.default_timezone
51
- self.enable_datetime_select_extension!
52
- self.load_error_messages
47
+ self.use_time_zones = ::ActiveRecord::Base.time_zone_aware_attributes rescue false
48
+ load_error_messages
53
49
  end
54
50
  end
55
51
  end
@@ -39,17 +39,17 @@ describe ValidatesTimeliness::ActiveRecord::AttributeMethods do
39
39
  end
40
40
 
41
41
  it "should call parser on write for datetime attribute" do
42
- @person.class.should_receive(:parse_date_time).once
42
+ ValidatesTimeliness::Parser.should_receive(:parse).once
43
43
  @person.birth_date_and_time = "2000-01-01 02:03:04"
44
44
  end
45
45
 
46
46
  it "should call parser on write for date attribute" do
47
- @person.class.should_receive(:parse_date_time).once
47
+ ValidatesTimeliness::Parser.should_receive(:parse).once
48
48
  @person.birth_date = "2000-01-01"
49
49
  end
50
50
 
51
51
  it "should call parser on write for time attribute" do
52
- @person.class.should_receive(:parse_date_time).once
52
+ ValidatesTimeliness::Parser.should_receive(:parse).once
53
53
  @person.birth_time = "12:00"
54
54
  end
55
55
 
@@ -221,4 +221,14 @@ describe ValidatesTimeliness::ActiveRecord::AttributeMethods do
221
221
  @person.birth_date.should == tomorrow
222
222
  end
223
223
 
224
+ it "should skip storing value in attributes hash on read if record frozen" do
225
+ @person = Person.new
226
+ @person.birth_date = Date.today
227
+ @person.save!
228
+ @person.reload
229
+ @person.freeze
230
+ @person.frozen?.should be_true
231
+ lambda { @person.birth_date }.should_not raise_error
232
+ @person.birth_date.should == Date.today
233
+ end
224
234
  end
data/spec/formats_spec.rb CHANGED
@@ -6,46 +6,6 @@ describe ValidatesTimeliness::Formats do
6
6
  before do
7
7
  @formats = ValidatesTimeliness::Formats
8
8
  end
9
-
10
- describe "expression generator" do
11
- it "should generate regexp for time" do
12
- generate_regexp_str('hh:nn:ss').should == '/(\d{2}):(\d{2}):(\d{2})/'
13
- end
14
-
15
- it "should generate regexp for time with meridian" do
16
- generate_regexp_str('hh:nn:ss ampm').should == '/(\d{2}):(\d{2}):(\d{2}) ((?:[aApP])\.?[mM]\.?)/'
17
- end
18
-
19
- it "should generate regexp for time with meridian and optional space between" do
20
- generate_regexp_str('hh:nn:ss_ampm').should == '/(\d{2}):(\d{2}):(\d{2})\s?((?:[aApP])\.?[mM]\.?)/'
21
- end
22
-
23
- it "should generate regexp for time with single or double digits" do
24
- generate_regexp_str('h:n:s').should == '/(\d{1,2}):(\d{1,2}):(\d{1,2})/'
25
- end
26
-
27
- it "should generate regexp for date" do
28
- generate_regexp_str('yyyy-mm-dd').should == '/(\d{4})-(\d{2})-(\d{2})/'
29
- end
30
-
31
- it "should generate regexp for date with slashes" do
32
- generate_regexp_str('dd/mm/yyyy').should == '/(\d{2})\/(\d{2})\/(\d{4})/'
33
- end
34
-
35
- it "should generate regexp for date with dots" do
36
- generate_regexp_str('dd.mm.yyyy').should == '/(\d{2})\.(\d{2})\.(\d{4})/'
37
- end
38
-
39
- it "should generate regexp for Ruby time string" do
40
- expected = '/(\w{3,9}) (\w{3,9}) (\d{2}):(\d{2}):(\d{2}) (?:[+-]\d{2}:?\d{2}) (\d{4})/'
41
- generate_regexp_str('ddd mmm hh:nn:ss zo yyyy').should == expected
42
- end
43
-
44
- it "should generate regexp for iso8601 datetime" do
45
- expected = '/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:Z|(?:[+-]\d{2}:?\d{2}))/'
46
- generate_regexp_str('yyyy-mm-ddThh:nn:ss(?:Z|zo)').should == expected
47
- end
48
- end
49
9
 
50
10
  describe "format proc generator" do
51
11
  it "should generate proc which outputs date array with values in correct order" do
@@ -71,6 +31,10 @@ describe ValidatesTimeliness::Formats do
71
31
  it "should generate proc which outputs time array with microseconds" do
72
32
  generate_proc('hh:nn:ss.u').call('01', '02', '03', '99').should == [0,0,0,1,2,3,990000]
73
33
  end
34
+
35
+ it "should generate proc which outputs datetime array with zone offset" do
36
+ generate_proc('yyyy-mm-dd hh:nn:ss.u zo').call('2001', '02', '03', '04', '05', '06', '99', '+10:00').should == [2001,2,3,4,5,6,990000,36000]
37
+ end
74
38
  end
75
39
 
76
40
  describe "validation regexps" do
@@ -136,49 +100,62 @@ describe ValidatesTimeliness::Formats do
136
100
  end
137
101
  end
138
102
 
139
- describe "extracting values" do
103
+ describe "parse" do
140
104
 
141
105
  it "should return time array from date string" do
142
- time_array = formats.parse('12:13:14', :time, true)
106
+ time_array = formats.parse('12:13:14', :time, :strict => true)
143
107
  time_array.should == [0,0,0,12,13,14,0]
144
108
  end
145
109
 
146
110
  it "should return date array from time string" do
147
- time_array = formats.parse('2000-02-01', :date, true)
111
+ time_array = formats.parse('2000-02-01', :date, :strict => true)
148
112
  time_array.should == [2000,2,1,0,0,0,0]
149
113
  end
150
114
 
151
115
  it "should return datetime array from string value" do
152
- time_array = formats.parse('2000-02-01 12:13:14', :datetime, true)
116
+ time_array = formats.parse('2000-02-01 12:13:14', :datetime, :strict => true)
153
117
  time_array.should == [2000,2,1,12,13,14,0]
154
118
  end
155
119
 
156
120
  it "should parse date string when type is datetime" do
157
- time_array = formats.parse('2000-02-01', :datetime, false)
121
+ time_array = formats.parse('2000-02-01', :datetime, :strict => false)
158
122
  time_array.should == [2000,2,1,0,0,0,0]
159
123
  end
160
124
 
161
125
  it "should ignore time when extracting date and strict is false" do
162
- time_array = formats.parse('2000-02-01 12:12', :date, false)
126
+ time_array = formats.parse('2000-02-01 12:13', :date, :strict => false)
163
127
  time_array.should == [2000,2,1,0,0,0,0]
164
128
  end
165
129
 
166
130
  it "should ignore time when extracting date from format with trailing year and strict is false" do
167
- time_array = formats.parse('01-02-2000 12:12', :date, false)
131
+ time_array = formats.parse('01-02-2000 12:13', :date, :strict => false)
168
132
  time_array.should == [2000,2,1,0,0,0,0]
169
133
  end
170
134
 
171
135
  it "should ignore date when extracting time and strict is false" do
172
- time_array = formats.parse('2000-02-01 12:12', :time, false)
173
- time_array.should == [0,0,0,12,12,0,0]
136
+ time_array = formats.parse('2000-02-01 12:13', :time, :strict => false)
137
+ time_array.should == [0,0,0,12,13,0,0]
138
+ end
139
+
140
+ it "should return zone offset when :include_offset options is true" do
141
+ time_array = formats.parse('2000-02-01T12:13:14-10:30', :datetime, :include_offset => true)
142
+ time_array.should == [2000,2,1,12,13,14,0,-37800]
174
143
  end
175
144
  end
176
145
 
177
- describe "removing formats" do
178
- before do
179
- formats.compile_format_expressions
146
+ describe "parse with format option" do
147
+ it "should return values if string matches specified format" do
148
+ time_array = formats.parse('2000-02-01 12:13:14', :datetime, :format => 'yyyy-mm-dd hh:nn:ss')
149
+ time_array.should == [2000,2,1,12,13,14,0]
180
150
  end
181
-
151
+
152
+ it "should return nil if string does not match specified format" do
153
+ time_array = formats.parse('2000-02-01 12:13', :datetime, :format => 'yyyy-mm-dd hh:nn:ss')
154
+ time_array.should be_nil
155
+ end
156
+ end
157
+
158
+ describe "removing formats" do
182
159
  it "should remove format from format array" do
183
160
  formats.remove_formats(:time, 'h.nn_ampm')
184
161
  formats.time_formats.should_not include("h o'clock")
@@ -196,7 +173,7 @@ describe ValidatesTimeliness::Formats do
196
173
 
197
174
  after do
198
175
  formats.time_formats << 'h.nn_ampm'
199
- # reload class instead
176
+ formats.compile_format_expressions
200
177
  end
201
178
  end
202
179
 
@@ -254,7 +231,7 @@ describe ValidatesTimeliness::Formats do
254
231
 
255
232
  def validate(time_string, type)
256
233
  valid = false
257
- formats.send("#{type}_expressions").each do |(regexp, processor)|
234
+ formats.send("#{type}_expressions").each do |format, regexp, processor|
258
235
  valid = true and break if /\A#{regexp}\Z/ =~ time_string
259
236
  end
260
237
  valid
@@ -1,39 +1,39 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
2
 
3
- describe ValidatesTimeliness::ValidationMethods do
3
+ describe ValidatesTimeliness::Parser do
4
4
  attr_accessor :person
5
5
 
6
- describe "parse_date_time" do
6
+ describe "parse" do
7
7
  it "should return time object for valid time string" do
8
- parse_method("2000-01-01 12:13:14", :datetime).should be_kind_of(Time)
8
+ parse("2000-01-01 12:13:14", :datetime).should be_kind_of(Time)
9
9
  end
10
10
 
11
11
  it "should return nil for time string with invalid date part" do
12
- parse_method("2000-02-30 12:13:14", :datetime).should be_nil
12
+ parse("2000-02-30 12:13:14", :datetime).should be_nil
13
13
  end
14
14
 
15
15
  it "should return nil for time string with invalid time part" do
16
- parse_method("2000-02-01 25:13:14", :datetime).should be_nil
16
+ parse("2000-02-01 25:13:14", :datetime).should be_nil
17
17
  end
18
18
 
19
19
  it "should return Time object when passed a Time object" do
20
- parse_method(Time.now, :datetime).should be_kind_of(Time)
20
+ parse(Time.now, :datetime).should be_kind_of(Time)
21
21
  end
22
22
 
23
23
  if RAILS_VER >= '2.1'
24
24
  it "should convert time string into current timezone" do
25
25
  Time.zone = 'Melbourne'
26
- time = parse_method("2000-01-01 12:13:14", :datetime)
26
+ time = parse("2000-01-01 12:13:14", :datetime)
27
27
  Time.zone.utc_offset.should == 10.hours
28
28
  end
29
29
  end
30
30
 
31
31
  it "should return nil for invalid date string" do
32
- parse_method("2000-02-30", :date).should be_nil
32
+ parse("2000-02-30", :date).should be_nil
33
33
  end
34
34
 
35
- def parse_method(*args)
36
- ActiveRecord::Base.parse_date_time(*args)
35
+ def parse(*args)
36
+ ValidatesTimeliness::Parser.parse(*args)
37
37
  end
38
38
  end
39
39
 
@@ -43,14 +43,14 @@ describe ValidatesTimeliness::ValidationMethods do
43
43
 
44
44
  it "should create time using current timezone" do
45
45
  Time.zone = 'Melbourne'
46
- time = ActiveRecord::Base.send(:make_time, [2000,1,1,12,0,0])
46
+ time = ValidatesTimeliness::Parser.make_time([2000,1,1,12,0,0])
47
47
  time.zone.should == "EST"
48
48
  end
49
49
 
50
50
  else
51
51
 
52
52
  it "should create time using default timezone" do
53
- time = ActiveRecord::Base.send(:make_time, [2000,1,1,12,0,0])
53
+ time = ValidatesTimeliness::Parser.make_time([2000,1,1,12,0,0])
54
54
  time.zone.should == "UTC"
55
55
  end
56
56
 
@@ -5,6 +5,7 @@ end
5
5
 
6
6
  class WithValidation < Person
7
7
  validates_date :birth_date,
8
+ :equal_to => '2000-01-01',
8
9
  :before => '2000-01-10',
9
10
  :after => '2000-01-01',
10
11
  :on_or_before => '2000-01-09',
@@ -12,6 +13,7 @@ class WithValidation < Person
12
13
  :between => ['2000-01-01', '2000-01-03']
13
14
 
14
15
  validates_time :birth_time,
16
+ :equal_to => '09:00',
15
17
  :before => '23:00',
16
18
  :after => '09:00',
17
19
  :on_or_before => '22:00',
@@ -19,6 +21,7 @@ class WithValidation < Person
19
21
  :between => ['09:00', '17:00']
20
22
 
21
23
  validates_datetime :birth_date_and_time,
24
+ :equal_to => '2000-01-01 09:00',
22
25
  :before => '2000-01-10 23:00',
23
26
  :after => '2000-01-01 09:00',
24
27
  :on_or_before => '2000-01-09 23:00',
@@ -61,6 +64,29 @@ describe "ValidateTimeliness matcher" do
61
64
  end
62
65
  end
63
66
 
67
+ describe "with equal_to option" do
68
+ test_values = {
69
+ :date => ['2000-01-01', '2000-01-02'],
70
+ :time => ['09:00', '09:01'],
71
+ :datetime => ['2000-01-01 09:00', '2000-01-01 09:01']
72
+ }
73
+
74
+ [:date, :time, :datetime].each do |type|
75
+
76
+ it "should report that #{type} is validated" do
77
+ with_validation.should self.send("validate_#{type}", attribute_for_type(type), :equal_to => test_values[type][0])
78
+ end
79
+
80
+ it "should report that #{type} is not validated when option value is incorrect" do
81
+ with_validation.should_not self.send("validate_#{type}", attribute_for_type(type), :equal_to => test_values[type][1])
82
+ end
83
+
84
+ it "should report that #{type} is not validated with option" do
85
+ no_validation.should_not self.send("validate_#{type}", attribute_for_type(type), :equal_to => test_values[type][0])
86
+ end
87
+ end
88
+ end
89
+
64
90
  describe "with before option" do
65
91
  test_values = {
66
92
  :date => ['2000-01-10', '2000-01-11'],
data/spec/spec_helper.rb CHANGED
@@ -39,13 +39,15 @@ ActiveRecord::Base.default_timezone = :utc
39
39
  RAILS_VER = Rails::VERSION::STRING
40
40
  puts "Using #{vendored ? 'vendored' : 'gem'} Rails version #{RAILS_VER} (ActiveRecord version #{ActiveRecord::VERSION::STRING})"
41
41
 
42
- require 'validates_timeliness'
43
-
44
42
  if RAILS_VER >= '2.1'
45
43
  Time.zone_default = ActiveSupport::TimeZone['UTC']
46
44
  ActiveRecord::Base.time_zone_aware_attributes = true
47
45
  end
48
46
 
47
+ require 'validates_timeliness'
48
+
49
+ ValidatesTimeliness.enable_datetime_select_extension!
50
+
49
51
  ActiveRecord::Migration.verbose = false
50
52
  ActiveRecord::Base.establish_connection({:adapter => 'sqlite3', :database => ':memory:'})
51
53
 
@@ -66,7 +66,7 @@ describe ValidatesTimeliness::Validator do
66
66
 
67
67
  it "should return array of Time objects when restriction is array of strings" do
68
68
  time1, time2 = "2000-01-02", "2000-01-01"
69
- evaluate_option_value([time1, time2], :datetime).should == [Person.parse_date_time(time2, :datetime), Person.parse_date_time(time1, :datetime)]
69
+ evaluate_option_value([time1, time2], :datetime).should == [parse(time2, :datetime), parse(time1, :datetime)]
70
70
  end
71
71
 
72
72
  it "should return array of Time objects when restriction is Range of Time objects" do
@@ -76,7 +76,7 @@ describe ValidatesTimeliness::Validator do
76
76
 
77
77
  it "should return array of Time objects when restriction is Range of time strings" do
78
78
  time1, time2 = "2000-01-02", "2000-01-01"
79
- evaluate_option_value(time1..time2, :datetime).should == [Person.parse_date_time(time2, :datetime), Person.parse_date_time(time1, :datetime)]
79
+ evaluate_option_value(time1..time2, :datetime).should == [parse(time2, :datetime), parse(time1, :datetime)]
80
80
  end
81
81
  def evaluate_option_value(restriction, type)
82
82
  configure_validator(:type => type)
@@ -473,6 +473,20 @@ describe ValidatesTimeliness::Validator do
473
473
  end
474
474
  end
475
475
 
476
+ describe "instance with format option" do
477
+ it "should validate attribute when value matches format" do
478
+ configure_validator(:type => :time, :format => 'hh:nn:ss')
479
+ validate_with(:birth_time, "12:00:00")
480
+ should_have_no_error(:birth_time, :invalid_time)
481
+ end
482
+
483
+ it "should not validate attribute when value does not match format" do
484
+ configure_validator(:type => :time, :format => 'hh:nn:ss')
485
+ validate_with(:birth_time, "12:00")
486
+ should_have_error(:birth_time, :invalid_time)
487
+ end
488
+ end
489
+
476
490
  describe "custom_error_messages" do
477
491
  it "should return hash of custom error messages from configuration with _message truncated from keys" do
478
492
  configure_validator(:type => :date, :invalid_date_message => 'thats no date')
@@ -554,12 +568,18 @@ describe ValidatesTimeliness::Validator do
554
568
  describe "custom formats" do
555
569
 
556
570
  before :all do
557
- @@formats = ValidatesTimeliness::Validator.error_value_formats
558
- ValidatesTimeliness::Validator.error_value_formats = {
571
+ custom = {
559
572
  :time => '%H:%M %p',
560
573
  :date => '%d-%m-%Y',
561
574
  :datetime => '%d-%m-%Y %H:%M %p'
562
575
  }
576
+
577
+ if defined?(I18n)
578
+ I18n.backend.store_translations 'en', :validates_timeliness => { :error_value_formats => custom }
579
+ else
580
+ @@formats = ValidatesTimeliness::Validator.error_value_formats
581
+ ValidatesTimeliness::Validator.error_value_formats = custom
582
+ end
563
583
  end
564
584
 
565
585
  it "should format datetime value of restriction" do
@@ -581,12 +601,20 @@ describe ValidatesTimeliness::Validator do
581
601
  end
582
602
 
583
603
  after :all do
584
- ValidatesTimeliness::Validator.error_value_formats = @@formats
604
+ if defined?(I18n)
605
+ I18n.reload!
606
+ else
607
+ ValidatesTimeliness::Validator.error_value_formats = @@formats
608
+ end
585
609
  end
586
610
  end
587
611
 
588
612
  end
589
613
 
614
+ def parse(*args)
615
+ ValidatesTimeliness::Parser.parse(*args)
616
+ end
617
+
590
618
  def configure_validator(options={})
591
619
  @validator = ValidatesTimeliness::Validator.new(options)
592
620
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: validates_timeliness
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.7
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Meehan
@@ -9,7 +9,7 @@ autorequire: validates_timeliness
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-03-26 00:00:00 +11:00
12
+ date: 2009-04-12 00:00:00 +10:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -43,6 +43,7 @@ files:
43
43
  - lib/validates_timeliness/active_record
44
44
  - lib/validates_timeliness/active_record/attribute_methods.rb
45
45
  - lib/validates_timeliness/active_record/multiparameter_attributes.rb
46
+ - lib/validates_timeliness/parser.rb
46
47
  - lib/validates_timeliness/formats.rb
47
48
  - lib/validates_timeliness/validator.rb
48
49
  - lib/validates_timeliness/spec
@@ -56,7 +57,6 @@ files:
56
57
  - spec/action_view
57
58
  - spec/action_view/instance_tag_spec.rb
58
59
  - spec/ginger_scenarios.rb
59
- - spec/validation_methods_spec.rb
60
60
  - spec/spec_helper.rb
61
61
  - spec/formats_spec.rb
62
62
  - spec/active_record
@@ -66,6 +66,7 @@ files:
66
66
  - spec/time_travel/time_travel.rb
67
67
  - spec/time_travel/time_extensions.rb
68
68
  - spec/time_travel/MIT-LICENSE
69
+ - spec/parser_spec.rb
69
70
  - spec/spec
70
71
  - spec/spec/rails
71
72
  - spec/spec/rails/matchers