activesupport 6.0.3.4 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activesupport might be problematic. Click here for more details.

Files changed (131) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +342 -472
  3. data/MIT-LICENSE +1 -1
  4. data/lib/active_support.rb +13 -1
  5. data/lib/active_support/array_inquirer.rb +4 -2
  6. data/lib/active_support/backtrace_cleaner.rb +3 -3
  7. data/lib/active_support/benchmarkable.rb +1 -1
  8. data/lib/active_support/cache.rb +85 -44
  9. data/lib/active_support/cache/file_store.rb +4 -3
  10. data/lib/active_support/cache/mem_cache_store.rb +21 -14
  11. data/lib/active_support/cache/memory_store.rb +46 -26
  12. data/lib/active_support/cache/redis_cache_store.rb +27 -27
  13. data/lib/active_support/cache/strategy/local_cache.rb +21 -6
  14. data/lib/active_support/callbacks.rb +65 -56
  15. data/lib/active_support/concern.rb +46 -2
  16. data/lib/active_support/configurable.rb +3 -3
  17. data/lib/active_support/configuration_file.rb +46 -0
  18. data/lib/active_support/core_ext/benchmark.rb +2 -2
  19. data/lib/active_support/core_ext/class/attribute.rb +34 -44
  20. data/lib/active_support/core_ext/class/subclasses.rb +17 -38
  21. data/lib/active_support/core_ext/date/conversions.rb +2 -1
  22. data/lib/active_support/core_ext/date_and_time/calculations.rb +13 -0
  23. data/lib/active_support/core_ext/date_and_time/compatibility.rb +15 -0
  24. data/lib/active_support/core_ext/enumerable.rb +76 -4
  25. data/lib/active_support/core_ext/hash/conversions.rb +2 -2
  26. data/lib/active_support/core_ext/hash/deep_transform_values.rb +1 -1
  27. data/lib/active_support/core_ext/hash/except.rb +1 -1
  28. data/lib/active_support/core_ext/hash/keys.rb +1 -1
  29. data/lib/active_support/core_ext/hash/slice.rb +3 -2
  30. data/lib/active_support/core_ext/load_error.rb +1 -1
  31. data/lib/active_support/core_ext/marshal.rb +2 -0
  32. data/lib/active_support/core_ext/module/attr_internal.rb +2 -2
  33. data/lib/active_support/core_ext/module/attribute_accessors.rb +23 -29
  34. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +8 -4
  35. data/lib/active_support/core_ext/module/concerning.rb +8 -2
  36. data/lib/active_support/core_ext/module/delegation.rb +38 -28
  37. data/lib/active_support/core_ext/module/introspection.rb +1 -25
  38. data/lib/active_support/core_ext/name_error.rb +29 -2
  39. data/lib/active_support/core_ext/numeric/conversions.rb +22 -18
  40. data/lib/active_support/core_ext/object/deep_dup.rb +1 -1
  41. data/lib/active_support/core_ext/object/json.rb +6 -2
  42. data/lib/active_support/core_ext/object/try.rb +2 -2
  43. data/lib/active_support/core_ext/range/compare_range.rb +9 -3
  44. data/lib/active_support/core_ext/range/include_time_with_zone.rb +8 -3
  45. data/lib/active_support/core_ext/regexp.rb +8 -1
  46. data/lib/active_support/core_ext/string/access.rb +5 -24
  47. data/lib/active_support/core_ext/string/conversions.rb +1 -0
  48. data/lib/active_support/core_ext/string/inflections.rb +38 -4
  49. data/lib/active_support/core_ext/string/inquiry.rb +1 -0
  50. data/lib/active_support/core_ext/string/multibyte.rb +2 -2
  51. data/lib/active_support/core_ext/string/output_safety.rb +9 -9
  52. data/lib/active_support/core_ext/string/starts_ends_with.rb +2 -2
  53. data/lib/active_support/core_ext/symbol.rb +3 -0
  54. data/lib/active_support/core_ext/symbol/starts_ends_with.rb +14 -0
  55. data/lib/active_support/core_ext/time/calculations.rb +19 -1
  56. data/lib/active_support/core_ext/time/conversions.rb +1 -0
  57. data/lib/active_support/core_ext/uri.rb +5 -1
  58. data/lib/active_support/current_attributes.rb +7 -2
  59. data/lib/active_support/current_attributes/test_helper.rb +13 -0
  60. data/lib/active_support/dependencies.rb +43 -19
  61. data/lib/active_support/deprecation.rb +6 -1
  62. data/lib/active_support/deprecation/behaviors.rb +15 -2
  63. data/lib/active_support/deprecation/disallowed.rb +56 -0
  64. data/lib/active_support/deprecation/instance_delegator.rb +0 -1
  65. data/lib/active_support/deprecation/method_wrappers.rb +3 -2
  66. data/lib/active_support/deprecation/proxy_wrappers.rb +3 -3
  67. data/lib/active_support/deprecation/reporting.rb +50 -7
  68. data/lib/active_support/descendants_tracker.rb +6 -2
  69. data/lib/active_support/duration.rb +71 -22
  70. data/lib/active_support/duration/iso8601_serializer.rb +15 -9
  71. data/lib/active_support/encrypted_file.rb +19 -2
  72. data/lib/active_support/environment_inquirer.rb +20 -0
  73. data/lib/active_support/evented_file_update_checker.rb +69 -133
  74. data/lib/active_support/fork_tracker.rb +62 -0
  75. data/lib/active_support/gem_version.rb +3 -3
  76. data/lib/active_support/hash_with_indifferent_access.rb +42 -23
  77. data/lib/active_support/i18n_railtie.rb +14 -19
  78. data/lib/active_support/inflector/inflections.rb +1 -2
  79. data/lib/active_support/inflector/methods.rb +35 -31
  80. data/lib/active_support/inflector/transliterate.rb +4 -4
  81. data/lib/active_support/json/decoding.rb +4 -4
  82. data/lib/active_support/json/encoding.rb +5 -1
  83. data/lib/active_support/key_generator.rb +1 -1
  84. data/lib/active_support/locale/en.yml +7 -3
  85. data/lib/active_support/log_subscriber.rb +8 -0
  86. data/lib/active_support/logger.rb +1 -1
  87. data/lib/active_support/logger_silence.rb +2 -26
  88. data/lib/active_support/logger_thread_safe_level.rb +34 -12
  89. data/lib/active_support/message_encryptor.rb +4 -7
  90. data/lib/active_support/message_verifier.rb +5 -5
  91. data/lib/active_support/messages/metadata.rb +9 -1
  92. data/lib/active_support/messages/rotation_configuration.rb +2 -1
  93. data/lib/active_support/messages/rotator.rb +6 -5
  94. data/lib/active_support/multibyte/chars.rb +4 -42
  95. data/lib/active_support/multibyte/unicode.rb +9 -83
  96. data/lib/active_support/notifications.rb +31 -4
  97. data/lib/active_support/notifications/fanout.rb +23 -8
  98. data/lib/active_support/notifications/instrumenter.rb +6 -15
  99. data/lib/active_support/number_helper.rb +29 -14
  100. data/lib/active_support/number_helper/number_converter.rb +1 -1
  101. data/lib/active_support/number_helper/number_to_currency_converter.rb +3 -7
  102. data/lib/active_support/number_helper/number_to_human_converter.rb +1 -1
  103. data/lib/active_support/number_helper/number_to_human_size_converter.rb +1 -1
  104. data/lib/active_support/number_helper/number_to_rounded_converter.rb +3 -3
  105. data/lib/active_support/number_helper/rounding_helper.rb +12 -28
  106. data/lib/active_support/option_merger.rb +3 -2
  107. data/lib/active_support/ordered_options.rb +8 -2
  108. data/lib/active_support/parameter_filter.rb +15 -10
  109. data/lib/active_support/per_thread_registry.rb +1 -1
  110. data/lib/active_support/rails.rb +1 -4
  111. data/lib/active_support/railtie.rb +23 -1
  112. data/lib/active_support/secure_compare_rotator.rb +51 -0
  113. data/lib/active_support/security_utils.rb +19 -12
  114. data/lib/active_support/string_inquirer.rb +4 -2
  115. data/lib/active_support/subscriber.rb +12 -7
  116. data/lib/active_support/tagged_logging.rb +29 -4
  117. data/lib/active_support/testing/assertions.rb +18 -11
  118. data/lib/active_support/testing/parallelization.rb +12 -95
  119. data/lib/active_support/testing/parallelization/server.rb +78 -0
  120. data/lib/active_support/testing/parallelization/worker.rb +100 -0
  121. data/lib/active_support/testing/time_helpers.rb +40 -3
  122. data/lib/active_support/time_with_zone.rb +66 -42
  123. data/lib/active_support/values/time_zone.rb +20 -10
  124. data/lib/active_support/xml_mini/rexml.rb +8 -1
  125. metadata +36 -38
  126. data/lib/active_support/core_ext/array/prepend_and_append.rb +0 -5
  127. data/lib/active_support/core_ext/hash/compact.rb +0 -5
  128. data/lib/active_support/core_ext/hash/transform_values.rb +0 -5
  129. data/lib/active_support/core_ext/module/reachable.rb +0 -6
  130. data/lib/active_support/core_ext/numeric/inquiry.rb +0 -5
  131. data/lib/active_support/core_ext/range/include_range.rb +0 -9
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/kernel/singleton_class"
4
3
  require "active_support/core_ext/module/delegation"
5
4
 
6
5
  module ActiveSupport
@@ -56,11 +56,12 @@ module ActiveSupport
56
56
  mod = nil
57
57
 
58
58
  method_names.each do |method_name|
59
+ message = options[method_name]
59
60
  if target_module.method_defined?(method_name) || target_module.private_method_defined?(method_name)
60
61
  method = target_module.instance_method(method_name)
61
62
  target_module.module_eval do
62
63
  redefine_method(method_name) do |*args, &block|
63
- deprecator.deprecation_warning(method_name, options[method_name])
64
+ deprecator.deprecation_warning(method_name, message)
64
65
  method.bind(self).call(*args, &block)
65
66
  end
66
67
  ruby2_keywords(method_name) if respond_to?(:ruby2_keywords, true)
@@ -69,7 +70,7 @@ module ActiveSupport
69
70
  mod ||= Module.new
70
71
  mod.module_eval do
71
72
  define_method(method_name) do |*args, &block|
72
- deprecator.deprecation_warning(method_name, options[method_name])
73
+ deprecator.deprecation_warning(method_name, message)
73
74
  super(*args, &block)
74
75
  end
75
76
  ruby2_keywords(method_name) if respond_to?(:ruby2_keywords, true)
@@ -121,7 +121,7 @@ module ActiveSupport
121
121
  # (Backtrace information…)
122
122
  # ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
123
123
  class DeprecatedConstantProxy < Module
124
- def self.new(*args, **kwargs, &block)
124
+ def self.new(*args, **options, &block)
125
125
  object = args.first
126
126
 
127
127
  return object unless object
@@ -129,7 +129,7 @@ module ActiveSupport
129
129
  end
130
130
 
131
131
  def initialize(old_const, new_const, deprecator = ActiveSupport::Deprecation.instance, message: "#{old_const} is deprecated! Use #{new_const} instead.")
132
- require "active_support/inflector/methods"
132
+ Kernel.require "active_support/inflector/methods"
133
133
 
134
134
  @old_const = old_const
135
135
  @new_const = new_const
@@ -147,7 +147,7 @@ module ActiveSupport
147
147
 
148
148
  # Don't give a deprecation warning on methods that IRB may invoke
149
149
  # during tab-completion.
150
- delegate :hash, :instance_methods, :name, to: :target
150
+ delegate :hash, :instance_methods, :name, :respond_to?, to: :target
151
151
 
152
152
  # Returns the class of the new constant.
153
153
  #
@@ -6,7 +6,7 @@ module ActiveSupport
6
6
  class Deprecation
7
7
  module Reporting
8
8
  # Whether to print a message (silent mode)
9
- attr_accessor :silenced
9
+ attr_writer :silenced
10
10
  # Name of gem where method is deprecated
11
11
  attr_accessor :gem_name
12
12
 
@@ -20,7 +20,11 @@ module ActiveSupport
20
20
 
21
21
  callstack ||= caller_locations(2)
22
22
  deprecation_message(callstack, message).tap do |m|
23
- behavior.each { |b| b.call(m, callstack, deprecation_horizon, gem_name) }
23
+ if deprecation_disallowed?(message)
24
+ disallowed_behavior.each { |b| b.call(m, callstack, deprecation_horizon, gem_name) }
25
+ else
26
+ behavior.each { |b| b.call(m, callstack, deprecation_horizon, gem_name) }
27
+ end
24
28
  end
25
29
  end
26
30
 
@@ -33,11 +37,50 @@ module ActiveSupport
33
37
  # ActiveSupport::Deprecation.warn('something broke!')
34
38
  # end
35
39
  # # => nil
36
- def silence
37
- old_silenced, @silenced = @silenced, true
38
- yield
39
- ensure
40
- @silenced = old_silenced
40
+ def silence(&block)
41
+ @silenced_thread.bind(true, &block)
42
+ end
43
+
44
+ # Allow previously disallowed deprecation warnings within the block.
45
+ # <tt>allowed_warnings</tt> can be an array containing strings, symbols, or regular
46
+ # expressions. (Symbols are treated as strings). These are compared against
47
+ # the text of deprecation warning messages generated within the block.
48
+ # Matching warnings will be exempt from the rules set by
49
+ # +ActiveSupport::Deprecation.disallowed_warnings+
50
+ #
51
+ # The optional <tt>if:</tt> argument accepts a truthy/falsy value or an object that
52
+ # responds to <tt>.call</tt>. If truthy, then matching warnings will be allowed.
53
+ # If falsey then the method yields to the block without allowing the warning.
54
+ #
55
+ # ActiveSupport::Deprecation.disallowed_behavior = :raise
56
+ # ActiveSupport::Deprecation.disallowed_warnings = [
57
+ # "something broke"
58
+ # ]
59
+ #
60
+ # ActiveSupport::Deprecation.warn('something broke!')
61
+ # # => ActiveSupport::DeprecationException
62
+ #
63
+ # ActiveSupport::Deprecation.allow ['something broke'] do
64
+ # ActiveSupport::Deprecation.warn('something broke!')
65
+ # end
66
+ # # => nil
67
+ #
68
+ # ActiveSupport::Deprecation.allow ['something broke'], if: Rails.env.production? do
69
+ # ActiveSupport::Deprecation.warn('something broke!')
70
+ # end
71
+ # # => ActiveSupport::DeprecationException for dev/test, nil for production
72
+ def allow(allowed_warnings = :all, if: true, &block)
73
+ conditional = binding.local_variable_get(:if)
74
+ conditional = conditional.call if conditional.respond_to?(:call)
75
+ if conditional
76
+ @explicitly_allowed_warnings.bind(allowed_warnings, &block)
77
+ else
78
+ yield
79
+ end
80
+ end
81
+
82
+ def silenced
83
+ @silenced || @silenced_thread.value
41
84
  end
42
85
 
43
86
  def deprecation_warning(deprecated_method_name, message = nil, caller_backtrace = nil)
@@ -13,6 +13,7 @@ module ActiveSupport
13
13
  descendants = @@direct_descendants[klass]
14
14
  descendants ? descendants.to_a : []
15
15
  end
16
+ alias_method :subclasses, :direct_descendants
16
17
 
17
18
  def descendants(klass)
18
19
  arr = []
@@ -59,6 +60,7 @@ module ActiveSupport
59
60
  def direct_descendants
60
61
  DescendantsTracker.direct_descendants(self)
61
62
  end
63
+ alias_method :subclasses, :direct_descendants
62
64
 
63
65
  def descendants
64
66
  DescendantsTracker.descendants(self)
@@ -77,15 +79,17 @@ module ActiveSupport
77
79
  end
78
80
 
79
81
  def <<(klass)
80
- cleanup!
81
82
  @refs << WeakRef.new(klass)
82
83
  end
83
84
 
84
85
  def each
85
- @refs.each do |ref|
86
+ @refs.reject! do |ref|
86
87
  yield ref.__getobj__
88
+ false
87
89
  rescue WeakRef::RefError
90
+ true
88
91
  end
92
+ self
89
93
  end
90
94
 
91
95
  def refs_size
@@ -39,7 +39,7 @@ module ActiveSupport
39
39
 
40
40
  def +(other)
41
41
  if Duration === other
42
- seconds = value + other.parts[:seconds]
42
+ seconds = value + other.parts.fetch(:seconds, 0)
43
43
  new_parts = other.parts.merge(seconds: seconds)
44
44
  new_value = value + other.value
45
45
 
@@ -51,8 +51,8 @@ module ActiveSupport
51
51
 
52
52
  def -(other)
53
53
  if Duration === other
54
- seconds = value - other.parts[:seconds]
55
- new_parts = other.parts.map { |part, other_value| [part, -other_value] }.to_h
54
+ seconds = value - other.parts.fetch(:seconds, 0)
55
+ new_parts = other.parts.transform_values(&:-@)
56
56
  new_parts = new_parts.merge(seconds: seconds)
57
57
  new_value = value - other.value
58
58
 
@@ -64,7 +64,7 @@ module ActiveSupport
64
64
 
65
65
  def *(other)
66
66
  if Duration === other
67
- new_parts = other.parts.map { |part, other_value| [part, value * other_value] }.to_h
67
+ new_parts = other.parts.transform_values { |other_value| value * other_value }
68
68
  new_value = value * other.value
69
69
 
70
70
  Duration.new(new_value, new_parts)
@@ -147,31 +147,31 @@ module ActiveSupport
147
147
  end
148
148
 
149
149
  def seconds(value) #:nodoc:
150
- new(value, [[:seconds, value]])
150
+ new(value, seconds: value)
151
151
  end
152
152
 
153
153
  def minutes(value) #:nodoc:
154
- new(value * SECONDS_PER_MINUTE, [[:minutes, value]])
154
+ new(value * SECONDS_PER_MINUTE, minutes: value)
155
155
  end
156
156
 
157
157
  def hours(value) #:nodoc:
158
- new(value * SECONDS_PER_HOUR, [[:hours, value]])
158
+ new(value * SECONDS_PER_HOUR, hours: value)
159
159
  end
160
160
 
161
161
  def days(value) #:nodoc:
162
- new(value * SECONDS_PER_DAY, [[:days, value]])
162
+ new(value * SECONDS_PER_DAY, days: value)
163
163
  end
164
164
 
165
165
  def weeks(value) #:nodoc:
166
- new(value * SECONDS_PER_WEEK, [[:weeks, value]])
166
+ new(value * SECONDS_PER_WEEK, weeks: value)
167
167
  end
168
168
 
169
169
  def months(value) #:nodoc:
170
- new(value * SECONDS_PER_MONTH, [[:months, value]])
170
+ new(value * SECONDS_PER_MONTH, months: value)
171
171
  end
172
172
 
173
173
  def years(value) #:nodoc:
174
- new(value * SECONDS_PER_YEAR, [[:years, value]])
174
+ new(value * SECONDS_PER_YEAR, years: value)
175
175
  end
176
176
 
177
177
  # Creates a new Duration from a seconds value that is converted
@@ -181,6 +181,10 @@ module ActiveSupport
181
181
  # ActiveSupport::Duration.build(2716146).parts # => {:months=>1, :days=>1}
182
182
  #
183
183
  def build(value)
184
+ unless value.is_a?(::Numeric)
185
+ raise TypeError, "can't build an #{self.name} from a #{value.class.name}"
186
+ end
187
+
184
188
  parts = {}
185
189
  remainder = value.round(9)
186
190
 
@@ -206,8 +210,7 @@ module ActiveSupport
206
210
  end
207
211
 
208
212
  def initialize(value, parts) #:nodoc:
209
- @value, @parts = value, parts.to_h
210
- @parts.default = 0
213
+ @value, @parts = value, parts
211
214
  @parts.reject! { |k, v| v.zero? } unless value == 0
212
215
  end
213
216
 
@@ -236,13 +239,12 @@ module ActiveSupport
236
239
  # are treated as seconds.
237
240
  def +(other)
238
241
  if Duration === other
239
- parts = @parts.dup
240
- other.parts.each do |(key, value)|
241
- parts[key] += value
242
+ parts = @parts.merge(other.parts) do |_key, value, other_value|
243
+ value + other_value
242
244
  end
243
245
  Duration.new(value + other.value, parts)
244
246
  else
245
- seconds = @parts[:seconds] + other
247
+ seconds = @parts.fetch(:seconds, 0) + other
246
248
  Duration.new(value + other, @parts.merge(seconds: seconds))
247
249
  end
248
250
  end
@@ -256,9 +258,9 @@ module ActiveSupport
256
258
  # Multiplies this Duration by a Numeric and returns a new Duration.
257
259
  def *(other)
258
260
  if Scalar === other || Duration === other
259
- Duration.new(value * other.value, parts.map { |type, number| [type, number * other.value] })
261
+ Duration.new(value * other.value, parts.transform_values { |number| number * other.value })
260
262
  elsif Numeric === other
261
- Duration.new(value * other, parts.map { |type, number| [type, number * other] })
263
+ Duration.new(value * other, parts.transform_values { |number| number * other })
262
264
  else
263
265
  raise_type_error(other)
264
266
  end
@@ -267,11 +269,11 @@ module ActiveSupport
267
269
  # Divides this Duration by a Numeric and returns a new Duration.
268
270
  def /(other)
269
271
  if Scalar === other
270
- Duration.new(value / other.value, parts.map { |type, number| [type, number / other.value] })
272
+ Duration.new(value / other.value, parts.transform_values { |number| number / other.value })
271
273
  elsif Duration === other
272
274
  value / other.value
273
275
  elsif Numeric === other
274
- Duration.new(value / other, parts.map { |type, number| [type, number / other] })
276
+ Duration.new(value / other, parts.transform_values { |number| number / other })
275
277
  else
276
278
  raise_type_error(other)
277
279
  end
@@ -290,7 +292,11 @@ module ActiveSupport
290
292
  end
291
293
 
292
294
  def -@ #:nodoc:
293
- Duration.new(-value, parts.map { |type, number| [type, -number] })
295
+ Duration.new(-value, parts.transform_values(&:-@))
296
+ end
297
+
298
+ def +@ #:nodoc:
299
+ self
294
300
  end
295
301
 
296
302
  def is_a?(klass) #:nodoc:
@@ -343,6 +349,49 @@ module ActiveSupport
343
349
  def to_i
344
350
  @value.to_i
345
351
  end
352
+ alias :in_seconds :to_i
353
+
354
+ # Returns the amount of minutes a duration covers as a float
355
+ #
356
+ # 1.day.in_minutes # => 1440.0
357
+ def in_minutes
358
+ in_seconds / SECONDS_PER_MINUTE.to_f
359
+ end
360
+
361
+ # Returns the amount of hours a duration covers as a float
362
+ #
363
+ # 1.day.in_hours # => 24.0
364
+ def in_hours
365
+ in_seconds / SECONDS_PER_HOUR.to_f
366
+ end
367
+
368
+ # Returns the amount of days a duration covers as a float
369
+ #
370
+ # 12.hours.in_days # => 0.5
371
+ def in_days
372
+ in_seconds / SECONDS_PER_DAY.to_f
373
+ end
374
+
375
+ # Returns the amount of weeks a duration covers as a float
376
+ #
377
+ # 2.months.in_weeks # => 8.696
378
+ def in_weeks
379
+ in_seconds / SECONDS_PER_WEEK.to_f
380
+ end
381
+
382
+ # Returns the amount of months a duration covers as a float
383
+ #
384
+ # 9.weeks.in_months # => 2.07
385
+ def in_months
386
+ in_seconds / SECONDS_PER_MONTH.to_f
387
+ end
388
+
389
+ # Returns the amount of years a duration covers as a float
390
+ #
391
+ # 30.days.in_years # => 0.082
392
+ def in_years
393
+ in_seconds / SECONDS_PER_YEAR.to_f
394
+ end
346
395
 
347
396
  # Returns +true+ if +other+ is also a Duration instance, which has the
348
397
  # same parts as this one.
@@ -6,6 +6,8 @@ module ActiveSupport
6
6
  class Duration
7
7
  # Serializes duration to string according to ISO 8601 Duration format.
8
8
  class ISO8601Serializer # :nodoc:
9
+ DATE_COMPONENTS = %i(years months days)
10
+
9
11
  def initialize(duration, precision: nil)
10
12
  @duration = duration
11
13
  @precision = precision
@@ -13,14 +15,14 @@ module ActiveSupport
13
15
 
14
16
  # Builds and returns output string.
15
17
  def serialize
16
- parts, sign = normalize
18
+ parts = normalize
17
19
  return "PT0S" if parts.empty?
18
20
 
19
21
  output = +"P"
20
22
  output << "#{parts[:years]}Y" if parts.key?(:years)
21
23
  output << "#{parts[:months]}M" if parts.key?(:months)
22
- output << "#{parts[:weeks]}W" if parts.key?(:weeks)
23
24
  output << "#{parts[:days]}D" if parts.key?(:days)
25
+ output << "#{parts[:weeks]}W" if parts.key?(:weeks)
24
26
  time = +""
25
27
  time << "#{parts[:hours]}H" if parts.key?(:hours)
26
28
  time << "#{parts[:minutes]}M" if parts.key?(:minutes)
@@ -28,7 +30,7 @@ module ActiveSupport
28
30
  time << "#{sprintf(@precision ? "%0.0#{@precision}f" : '%g', parts[:seconds])}S"
29
31
  end
30
32
  output << "T#{time}" unless time.empty?
31
- "#{sign}#{output}"
33
+ output
32
34
  end
33
35
 
34
36
  private
@@ -40,13 +42,17 @@ module ActiveSupport
40
42
  parts = @duration.parts.each_with_object(Hash.new(0)) do |(k, v), p|
41
43
  p[k] += v unless v.zero?
42
44
  end
43
- # If all parts are negative - let's make a negative duration
44
- sign = ""
45
- if parts.values.all? { |v| v < 0 }
46
- sign = "-"
47
- parts.transform_values!(&:-@)
45
+
46
+ # Convert weeks to days and remove weeks if mixed with date parts
47
+ if week_mixed_with_date?(parts)
48
+ parts[:days] += parts.delete(:weeks) * SECONDS_PER_WEEK / SECONDS_PER_DAY
48
49
  end
49
- [parts, sign]
50
+
51
+ parts
52
+ end
53
+
54
+ def week_mixed_with_date?(parts)
55
+ parts.key?(:weeks) && (parts.keys & DATE_COMPONENTS).any?
50
56
  end
51
57
  end
52
58
  end
@@ -20,17 +20,28 @@ module ActiveSupport
20
20
  end
21
21
  end
22
22
 
23
+ class InvalidKeyLengthError < RuntimeError
24
+ def initialize
25
+ super "Encryption key must be exactly #{EncryptedFile.expected_key_length} characters."
26
+ end
27
+ end
28
+
23
29
  CIPHER = "aes-128-gcm"
24
30
 
25
31
  def self.generate_key
26
32
  SecureRandom.hex(ActiveSupport::MessageEncryptor.key_len(CIPHER))
27
33
  end
28
34
 
35
+ def self.expected_key_length # :nodoc:
36
+ @expected_key_length ||= generate_key.length
37
+ end
38
+
29
39
 
30
40
  attr_reader :content_path, :key_path, :env_key, :raise_if_missing_key
31
41
 
32
42
  def initialize(content_path:, key_path:, env_key:, raise_if_missing_key:)
33
- @content_path, @key_path = Pathname.new(content_path), Pathname.new(key_path)
43
+ @content_path = Pathname.new(content_path).yield_self { |path| path.symlink? ? path.realpath : path }
44
+ @key_path = Pathname.new(key_path)
34
45
  @env_key, @raise_if_missing_key = env_key, raise_if_missing_key
35
46
  end
36
47
 
@@ -73,6 +84,7 @@ module ActiveSupport
73
84
 
74
85
 
75
86
  def encrypt(contents)
87
+ check_key_length
76
88
  encryptor.encrypt_and_sign contents
77
89
  end
78
90
 
@@ -90,11 +102,16 @@ module ActiveSupport
90
102
  end
91
103
 
92
104
  def read_key_file
93
- key_path.binread.strip if key_path.exist?
105
+ return @key_file_contents if defined?(@key_file_contents)
106
+ @key_file_contents = (key_path.binread.strip if key_path.exist?)
94
107
  end
95
108
 
96
109
  def handle_missing_key
97
110
  raise MissingKeyError.new(key_path: key_path, env_key: env_key) if raise_if_missing_key
98
111
  end
112
+
113
+ def check_key_length
114
+ raise InvalidKeyLengthError if key&.length != self.class.expected_key_length
115
+ end
99
116
  end
100
117
  end