validates_timeliness 1.1.7 → 2.0.0

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