activesupport 6.0.6.1 → 7.1.3.2

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.
Files changed (245) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +865 -438
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +6 -6
  5. data/lib/active_support/actionable_error.rb +4 -2
  6. data/lib/active_support/array_inquirer.rb +4 -2
  7. data/lib/active_support/backtrace_cleaner.rb +30 -10
  8. data/lib/active_support/benchmarkable.rb +4 -3
  9. data/lib/active_support/broadcast_logger.rb +250 -0
  10. data/lib/active_support/builder.rb +1 -1
  11. data/lib/active_support/cache/coder.rb +153 -0
  12. data/lib/active_support/cache/entry.rb +134 -0
  13. data/lib/active_support/cache/file_store.rb +53 -20
  14. data/lib/active_support/cache/mem_cache_store.rb +208 -63
  15. data/lib/active_support/cache/memory_store.rb +120 -38
  16. data/lib/active_support/cache/null_store.rb +16 -2
  17. data/lib/active_support/cache/redis_cache_store.rb +201 -208
  18. data/lib/active_support/cache/serializer_with_fallback.rb +175 -0
  19. data/lib/active_support/cache/strategy/local_cache.rb +73 -66
  20. data/lib/active_support/cache.rb +539 -261
  21. data/lib/active_support/callbacks.rb +273 -142
  22. data/lib/active_support/code_generator.rb +65 -0
  23. data/lib/active_support/concern.rb +53 -7
  24. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +44 -7
  25. data/lib/active_support/concurrency/null_lock.rb +13 -0
  26. data/lib/active_support/concurrency/share_lock.rb +2 -2
  27. data/lib/active_support/configurable.rb +19 -6
  28. data/lib/active_support/configuration_file.rb +51 -0
  29. data/lib/active_support/core_ext/array/access.rb +1 -5
  30. data/lib/active_support/core_ext/array/conversions.rb +15 -13
  31. data/lib/active_support/core_ext/array/grouping.rb +6 -6
  32. data/lib/active_support/core_ext/array/inquiry.rb +2 -2
  33. data/lib/active_support/core_ext/benchmark.rb +2 -2
  34. data/lib/active_support/core_ext/big_decimal/conversions.rb +1 -1
  35. data/lib/active_support/core_ext/class/attribute.rb +34 -44
  36. data/lib/active_support/core_ext/class/subclasses.rb +19 -29
  37. data/lib/active_support/core_ext/date/blank.rb +1 -1
  38. data/lib/active_support/core_ext/date/calculations.rb +24 -9
  39. data/lib/active_support/core_ext/date/conversions.rb +18 -16
  40. data/lib/active_support/core_ext/date_and_time/calculations.rb +27 -4
  41. data/lib/active_support/core_ext/date_and_time/compatibility.rb +15 -0
  42. data/lib/active_support/core_ext/date_time/blank.rb +1 -1
  43. data/lib/active_support/core_ext/date_time/calculations.rb +4 -0
  44. data/lib/active_support/core_ext/date_time/conversions.rb +19 -15
  45. data/lib/active_support/core_ext/digest/uuid.rb +30 -13
  46. data/lib/active_support/core_ext/enumerable.rb +146 -72
  47. data/lib/active_support/core_ext/erb/util.rb +196 -0
  48. data/lib/active_support/core_ext/file/atomic.rb +3 -1
  49. data/lib/active_support/core_ext/hash/conversions.rb +3 -4
  50. data/lib/active_support/core_ext/hash/deep_merge.rb +22 -14
  51. data/lib/active_support/core_ext/hash/deep_transform_values.rb +4 -4
  52. data/lib/active_support/core_ext/hash/indifferent_access.rb +3 -3
  53. data/lib/active_support/core_ext/hash/keys.rb +5 -5
  54. data/lib/active_support/core_ext/hash/slice.rb +3 -2
  55. data/lib/active_support/core_ext/integer/inflections.rb +12 -12
  56. data/lib/active_support/core_ext/kernel/reporting.rb +4 -4
  57. data/lib/active_support/core_ext/kernel/singleton_class.rb +1 -1
  58. data/lib/active_support/core_ext/load_error.rb +1 -1
  59. data/lib/active_support/core_ext/module/attr_internal.rb +2 -2
  60. data/lib/active_support/core_ext/module/attribute_accessors.rb +31 -29
  61. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +51 -20
  62. data/lib/active_support/core_ext/module/concerning.rb +14 -8
  63. data/lib/active_support/core_ext/module/delegation.rb +75 -42
  64. data/lib/active_support/core_ext/module/deprecation.rb +15 -12
  65. data/lib/active_support/core_ext/module/introspection.rb +1 -26
  66. data/lib/active_support/core_ext/name_error.rb +23 -2
  67. data/lib/active_support/core_ext/numeric/bytes.rb +9 -0
  68. data/lib/active_support/core_ext/numeric/conversions.rb +82 -73
  69. data/lib/active_support/core_ext/object/acts_like.rb +29 -5
  70. data/lib/active_support/core_ext/object/blank.rb +2 -2
  71. data/lib/active_support/core_ext/object/deep_dup.rb +17 -1
  72. data/lib/active_support/core_ext/object/duplicable.rb +15 -4
  73. data/lib/active_support/core_ext/object/inclusion.rb +13 -5
  74. data/lib/active_support/core_ext/object/instance_variables.rb +22 -12
  75. data/lib/active_support/core_ext/object/json.rb +52 -28
  76. data/lib/active_support/core_ext/object/to_query.rb +2 -4
  77. data/lib/active_support/core_ext/object/try.rb +20 -20
  78. data/lib/active_support/core_ext/object/with.rb +44 -0
  79. data/lib/active_support/core_ext/object/with_options.rb +25 -6
  80. data/lib/active_support/core_ext/object.rb +1 -0
  81. data/lib/active_support/core_ext/pathname/blank.rb +16 -0
  82. data/lib/active_support/core_ext/pathname/existence.rb +23 -0
  83. data/lib/active_support/core_ext/pathname.rb +4 -0
  84. data/lib/active_support/core_ext/range/compare_range.rb +6 -25
  85. data/lib/active_support/core_ext/range/conversions.rb +34 -13
  86. data/lib/active_support/core_ext/range/each.rb +1 -1
  87. data/lib/active_support/core_ext/range/overlap.rb +40 -0
  88. data/lib/active_support/core_ext/range.rb +1 -2
  89. data/lib/active_support/core_ext/regexp.rb +8 -1
  90. data/lib/active_support/core_ext/securerandom.rb +25 -13
  91. data/lib/active_support/core_ext/string/access.rb +5 -24
  92. data/lib/active_support/core_ext/string/conversions.rb +3 -2
  93. data/lib/active_support/core_ext/string/filters.rb +21 -15
  94. data/lib/active_support/core_ext/string/indent.rb +1 -1
  95. data/lib/active_support/core_ext/string/inflections.rb +51 -10
  96. data/lib/active_support/core_ext/string/inquiry.rb +2 -1
  97. data/lib/active_support/core_ext/string/multibyte.rb +2 -2
  98. data/lib/active_support/core_ext/string/output_safety.rb +85 -194
  99. data/lib/active_support/core_ext/string/starts_ends_with.rb +2 -2
  100. data/lib/active_support/core_ext/symbol/starts_ends_with.rb +6 -0
  101. data/lib/active_support/core_ext/symbol.rb +3 -0
  102. data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
  103. data/lib/active_support/core_ext/time/calculations.rb +46 -8
  104. data/lib/active_support/core_ext/time/conversions.rb +16 -13
  105. data/lib/active_support/core_ext/time/zones.rb +12 -28
  106. data/lib/active_support/core_ext.rb +2 -1
  107. data/lib/active_support/current_attributes/test_helper.rb +13 -0
  108. data/lib/active_support/current_attributes.rb +54 -22
  109. data/lib/active_support/deep_mergeable.rb +53 -0
  110. data/lib/active_support/dependencies/autoload.rb +17 -12
  111. data/lib/active_support/dependencies/interlock.rb +10 -18
  112. data/lib/active_support/dependencies/require_dependency.rb +28 -0
  113. data/lib/active_support/dependencies.rb +58 -769
  114. data/lib/active_support/deprecation/behaviors.rb +77 -38
  115. data/lib/active_support/deprecation/constant_accessor.rb +5 -4
  116. data/lib/active_support/deprecation/deprecators.rb +104 -0
  117. data/lib/active_support/deprecation/disallowed.rb +54 -0
  118. data/lib/active_support/deprecation/instance_delegator.rb +31 -5
  119. data/lib/active_support/deprecation/method_wrappers.rb +12 -28
  120. data/lib/active_support/deprecation/proxy_wrappers.rb +40 -25
  121. data/lib/active_support/deprecation/reporting.rb +76 -16
  122. data/lib/active_support/deprecation.rb +36 -4
  123. data/lib/active_support/deprecator.rb +7 -0
  124. data/lib/active_support/descendants_tracker.rb +150 -68
  125. data/lib/active_support/digest.rb +5 -3
  126. data/lib/active_support/duration/iso8601_parser.rb +3 -3
  127. data/lib/active_support/duration/iso8601_serializer.rb +24 -12
  128. data/lib/active_support/duration.rb +136 -56
  129. data/lib/active_support/encrypted_configuration.rb +72 -9
  130. data/lib/active_support/encrypted_file.rb +46 -13
  131. data/lib/active_support/environment_inquirer.rb +40 -0
  132. data/lib/active_support/error_reporter/test_helper.rb +15 -0
  133. data/lib/active_support/error_reporter.rb +203 -0
  134. data/lib/active_support/evented_file_update_checker.rb +86 -137
  135. data/lib/active_support/execution_context/test_helper.rb +13 -0
  136. data/lib/active_support/execution_context.rb +53 -0
  137. data/lib/active_support/execution_wrapper.rb +31 -12
  138. data/lib/active_support/executor/test_helper.rb +7 -0
  139. data/lib/active_support/file_update_checker.rb +4 -2
  140. data/lib/active_support/fork_tracker.rb +79 -0
  141. data/lib/active_support/gem_version.rb +5 -5
  142. data/lib/active_support/gzip.rb +2 -0
  143. data/lib/active_support/hash_with_indifferent_access.rb +86 -42
  144. data/lib/active_support/html_safe_translation.rb +53 -0
  145. data/lib/active_support/i18n.rb +2 -1
  146. data/lib/active_support/i18n_railtie.rb +29 -27
  147. data/lib/active_support/inflector/inflections.rb +26 -9
  148. data/lib/active_support/inflector/methods.rb +54 -64
  149. data/lib/active_support/inflector/transliterate.rb +7 -5
  150. data/lib/active_support/isolated_execution_state.rb +76 -0
  151. data/lib/active_support/json/decoding.rb +6 -5
  152. data/lib/active_support/json/encoding.rb +31 -45
  153. data/lib/active_support/key_generator.rb +32 -7
  154. data/lib/active_support/lazy_load_hooks.rb +33 -7
  155. data/lib/active_support/locale/en.yml +10 -4
  156. data/lib/active_support/log_subscriber/test_helper.rb +2 -2
  157. data/lib/active_support/log_subscriber.rb +101 -32
  158. data/lib/active_support/logger.rb +9 -60
  159. data/lib/active_support/logger_silence.rb +2 -26
  160. data/lib/active_support/logger_thread_safe_level.rb +24 -25
  161. data/lib/active_support/message_encryptor.rb +205 -58
  162. data/lib/active_support/message_encryptors.rb +141 -0
  163. data/lib/active_support/message_pack/cache_serializer.rb +23 -0
  164. data/lib/active_support/message_pack/extensions.rb +292 -0
  165. data/lib/active_support/message_pack/serializer.rb +63 -0
  166. data/lib/active_support/message_pack.rb +50 -0
  167. data/lib/active_support/message_verifier.rb +237 -86
  168. data/lib/active_support/message_verifiers.rb +135 -0
  169. data/lib/active_support/messages/codec.rb +65 -0
  170. data/lib/active_support/messages/metadata.rb +112 -46
  171. data/lib/active_support/messages/rotation_configuration.rb +2 -1
  172. data/lib/active_support/messages/rotation_coordinator.rb +93 -0
  173. data/lib/active_support/messages/rotator.rb +35 -32
  174. data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
  175. data/lib/active_support/multibyte/chars.rb +15 -52
  176. data/lib/active_support/multibyte/unicode.rb +8 -122
  177. data/lib/active_support/multibyte.rb +1 -1
  178. data/lib/active_support/notifications/fanout.rb +310 -105
  179. data/lib/active_support/notifications/instrumenter.rb +113 -48
  180. data/lib/active_support/notifications.rb +56 -29
  181. data/lib/active_support/number_helper/number_converter.rb +15 -8
  182. data/lib/active_support/number_helper/number_to_currency_converter.rb +11 -6
  183. data/lib/active_support/number_helper/number_to_delimited_converter.rb +1 -1
  184. data/lib/active_support/number_helper/number_to_human_converter.rb +1 -1
  185. data/lib/active_support/number_helper/number_to_human_size_converter.rb +5 -5
  186. data/lib/active_support/number_helper/number_to_phone_converter.rb +2 -1
  187. data/lib/active_support/number_helper/number_to_rounded_converter.rb +9 -5
  188. data/lib/active_support/number_helper/rounding_helper.rb +12 -32
  189. data/lib/active_support/number_helper.rb +379 -304
  190. data/lib/active_support/option_merger.rb +11 -18
  191. data/lib/active_support/ordered_hash.rb +4 -4
  192. data/lib/active_support/ordered_options.rb +23 -3
  193. data/lib/active_support/parameter_filter.rb +104 -75
  194. data/lib/active_support/proxy_object.rb +2 -0
  195. data/lib/active_support/rails.rb +1 -4
  196. data/lib/active_support/railtie.rb +90 -6
  197. data/lib/active_support/reloader.rb +12 -4
  198. data/lib/active_support/rescuable.rb +18 -16
  199. data/lib/active_support/ruby_features.rb +7 -0
  200. data/lib/active_support/secure_compare_rotator.rb +58 -0
  201. data/lib/active_support/security_utils.rb +19 -12
  202. data/lib/active_support/string_inquirer.rb +5 -3
  203. data/lib/active_support/subscriber.rb +23 -47
  204. data/lib/active_support/syntax_error_proxy.rb +70 -0
  205. data/lib/active_support/tagged_logging.rb +84 -23
  206. data/lib/active_support/test_case.rb +166 -27
  207. data/lib/active_support/testing/assertions.rb +73 -20
  208. data/lib/active_support/testing/autorun.rb +0 -2
  209. data/lib/active_support/testing/constant_stubbing.rb +32 -0
  210. data/lib/active_support/testing/deprecation.rb +53 -2
  211. data/lib/active_support/testing/error_reporter_assertions.rb +107 -0
  212. data/lib/active_support/testing/isolation.rb +30 -29
  213. data/lib/active_support/testing/method_call_assertions.rb +24 -11
  214. data/lib/active_support/testing/parallelization/server.rb +82 -0
  215. data/lib/active_support/testing/parallelization/worker.rb +103 -0
  216. data/lib/active_support/testing/parallelization.rb +16 -95
  217. data/lib/active_support/testing/parallelize_executor.rb +81 -0
  218. data/lib/active_support/testing/stream.rb +4 -6
  219. data/lib/active_support/testing/strict_warnings.rb +39 -0
  220. data/lib/active_support/testing/tagged_logging.rb +1 -1
  221. data/lib/active_support/testing/time_helpers.rb +89 -19
  222. data/lib/active_support/time_with_zone.rb +105 -70
  223. data/lib/active_support/values/time_zone.rb +59 -26
  224. data/lib/active_support/version.rb +1 -1
  225. data/lib/active_support/xml_mini/jdom.rb +4 -11
  226. data/lib/active_support/xml_mini/libxml.rb +5 -5
  227. data/lib/active_support/xml_mini/libxmlsax.rb +1 -1
  228. data/lib/active_support/xml_mini/nokogiri.rb +5 -5
  229. data/lib/active_support/xml_mini/nokogirisax.rb +2 -2
  230. data/lib/active_support/xml_mini/rexml.rb +9 -2
  231. data/lib/active_support/xml_mini.rb +7 -6
  232. data/lib/active_support.rb +40 -1
  233. metadata +127 -40
  234. data/lib/active_support/core_ext/array/prepend_and_append.rb +0 -5
  235. data/lib/active_support/core_ext/hash/compact.rb +0 -5
  236. data/lib/active_support/core_ext/hash/transform_values.rb +0 -5
  237. data/lib/active_support/core_ext/marshal.rb +0 -24
  238. data/lib/active_support/core_ext/module/reachable.rb +0 -6
  239. data/lib/active_support/core_ext/numeric/inquiry.rb +0 -5
  240. data/lib/active_support/core_ext/range/include_range.rb +0 -9
  241. data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -23
  242. data/lib/active_support/core_ext/range/overlaps.rb +0 -10
  243. data/lib/active_support/core_ext/uri.rb +0 -25
  244. data/lib/active_support/dependencies/zeitwerk_integration.rb +0 -117
  245. data/lib/active_support/per_thread_registry.rb +0 -60
@@ -3,15 +3,16 @@
3
3
  require "active_support/core_ext/array/conversions"
4
4
  require "active_support/core_ext/module/delegation"
5
5
  require "active_support/core_ext/object/acts_like"
6
- require "active_support/core_ext/string/filters"
7
6
 
8
7
  module ActiveSupport
8
+ # = Active Support \Duration
9
+ #
9
10
  # Provides accurate date and time measurements using Date#advance and
10
11
  # Time#advance, respectively. It mainly supports the methods on Numeric.
11
12
  #
12
13
  # 1.month.ago # equivalent to Time.now.advance(months: -1)
13
14
  class Duration
14
- class Scalar < Numeric #:nodoc:
15
+ class Scalar < Numeric # :nodoc:
15
16
  attr_reader :value
16
17
  delegate :to_i, :to_f, :to_s, to: :value
17
18
 
@@ -39,11 +40,11 @@ module ActiveSupport
39
40
 
40
41
  def +(other)
41
42
  if Duration === other
42
- seconds = value + other.parts[:seconds]
43
- new_parts = other.parts.merge(seconds: seconds)
43
+ seconds = value + other._parts.fetch(:seconds, 0)
44
+ new_parts = other._parts.merge(seconds: seconds)
44
45
  new_value = value + other.value
45
46
 
46
- Duration.new(new_value, new_parts)
47
+ Duration.new(new_value, new_parts, other.variable?)
47
48
  else
48
49
  calculate(:+, other)
49
50
  end
@@ -51,12 +52,12 @@ module ActiveSupport
51
52
 
52
53
  def -(other)
53
54
  if Duration === other
54
- seconds = value - other.parts[:seconds]
55
- new_parts = other.parts.map { |part, other_value| [part, -other_value] }.to_h
55
+ seconds = value - other._parts.fetch(:seconds, 0)
56
+ new_parts = other._parts.transform_values(&:-@)
56
57
  new_parts = new_parts.merge(seconds: seconds)
57
58
  new_value = value - other.value
58
59
 
59
- Duration.new(new_value, new_parts)
60
+ Duration.new(new_value, new_parts, other.variable?)
60
61
  else
61
62
  calculate(:-, other)
62
63
  end
@@ -64,10 +65,10 @@ module ActiveSupport
64
65
 
65
66
  def *(other)
66
67
  if Duration === other
67
- new_parts = other.parts.map { |part, other_value| [part, value * other_value] }.to_h
68
+ new_parts = other._parts.transform_values { |other_value| value * other_value }
68
69
  new_value = value * other.value
69
70
 
70
- Duration.new(new_value, new_parts)
71
+ Duration.new(new_value, new_parts, other.variable?)
71
72
  else
72
73
  calculate(:*, other)
73
74
  end
@@ -89,6 +90,10 @@ module ActiveSupport
89
90
  end
90
91
  end
91
92
 
93
+ def variable? # :nodoc:
94
+ false
95
+ end
96
+
92
97
  private
93
98
  def calculate(op, other)
94
99
  if Scalar === other
@@ -123,8 +128,9 @@ module ActiveSupport
123
128
  }.freeze
124
129
 
125
130
  PARTS = [:years, :months, :weeks, :days, :hours, :minutes, :seconds].freeze
131
+ VARIABLE_PARTS = [:years, :months, :weeks, :days].freeze
126
132
 
127
- attr_accessor :value, :parts
133
+ attr_reader :value
128
134
 
129
135
  autoload :ISO8601Parser, "active_support/duration/iso8601_parser"
130
136
  autoload :ISO8601Serializer, "active_support/duration/iso8601_serializer"
@@ -140,38 +146,38 @@ module ActiveSupport
140
146
  new(calculate_total_seconds(parts), parts)
141
147
  end
142
148
 
143
- def ===(other) #:nodoc:
149
+ def ===(other) # :nodoc:
144
150
  other.is_a?(Duration)
145
151
  rescue ::NoMethodError
146
152
  false
147
153
  end
148
154
 
149
- def seconds(value) #:nodoc:
150
- new(value, [[:seconds, value]])
155
+ def seconds(value) # :nodoc:
156
+ new(value, { seconds: value }, false)
151
157
  end
152
158
 
153
- def minutes(value) #:nodoc:
154
- new(value * SECONDS_PER_MINUTE, [[:minutes, value]])
159
+ def minutes(value) # :nodoc:
160
+ new(value * SECONDS_PER_MINUTE, { minutes: value }, false)
155
161
  end
156
162
 
157
- def hours(value) #:nodoc:
158
- new(value * SECONDS_PER_HOUR, [[:hours, value]])
163
+ def hours(value) # :nodoc:
164
+ new(value * SECONDS_PER_HOUR, { hours: value }, false)
159
165
  end
160
166
 
161
- def days(value) #:nodoc:
162
- new(value * SECONDS_PER_DAY, [[:days, value]])
167
+ def days(value) # :nodoc:
168
+ new(value * SECONDS_PER_DAY, { days: value }, true)
163
169
  end
164
170
 
165
- def weeks(value) #:nodoc:
166
- new(value * SECONDS_PER_WEEK, [[:weeks, value]])
171
+ def weeks(value) # :nodoc:
172
+ new(value * SECONDS_PER_WEEK, { weeks: value }, true)
167
173
  end
168
174
 
169
- def months(value) #:nodoc:
170
- new(value * SECONDS_PER_MONTH, [[:months, value]])
175
+ def months(value) # :nodoc:
176
+ new(value * SECONDS_PER_MONTH, { months: value }, true)
171
177
  end
172
178
 
173
- def years(value) #:nodoc:
174
- new(value * SECONDS_PER_YEAR, [[:years, value]])
179
+ def years(value) # :nodoc:
180
+ new(value * SECONDS_PER_YEAR, { years: value }, true)
175
181
  end
176
182
 
177
183
  # Creates a new Duration from a seconds value that is converted
@@ -181,20 +187,30 @@ module ActiveSupport
181
187
  # ActiveSupport::Duration.build(2716146).parts # => {:months=>1, :days=>1}
182
188
  #
183
189
  def build(value)
190
+ unless value.is_a?(::Numeric)
191
+ raise TypeError, "can't build an #{self.name} from a #{value.class.name}"
192
+ end
193
+
184
194
  parts = {}
185
- remainder = value.round(9)
195
+ remainder_sign = value <=> 0
196
+ remainder = value.round(9).abs
197
+ variable = false
186
198
 
187
199
  PARTS.each do |part|
188
200
  unless part == :seconds
189
201
  part_in_seconds = PARTS_IN_SECONDS[part]
190
- parts[part] = remainder.div(part_in_seconds)
202
+ parts[part] = remainder.div(part_in_seconds) * remainder_sign
191
203
  remainder %= part_in_seconds
204
+
205
+ unless parts[part].zero?
206
+ variable ||= VARIABLE_PARTS.include?(part)
207
+ end
192
208
  end
193
209
  end unless value == 0
194
210
 
195
- parts[:seconds] = remainder
211
+ parts[:seconds] = remainder * remainder_sign
196
212
 
197
- new(value, parts)
213
+ new(value, parts, variable)
198
214
  end
199
215
 
200
216
  private
@@ -205,13 +221,23 @@ module ActiveSupport
205
221
  end
206
222
  end
207
223
 
208
- def initialize(value, parts) #:nodoc:
209
- @value, @parts = value, parts.to_h
210
- @parts.default = 0
224
+ def initialize(value, parts, variable = nil) # :nodoc:
225
+ @value, @parts = value, parts
211
226
  @parts.reject! { |k, v| v.zero? } unless value == 0
227
+ @parts.freeze
228
+ @variable = variable
229
+
230
+ if @variable.nil?
231
+ @variable = @parts.any? { |part, _| VARIABLE_PARTS.include?(part) }
232
+ end
233
+ end
234
+
235
+ # Returns a copy of the parts hash that defines the duration
236
+ def parts
237
+ @parts.dup
212
238
  end
213
239
 
214
- def coerce(other) #:nodoc:
240
+ def coerce(other) # :nodoc:
215
241
  case other
216
242
  when Scalar
217
243
  [other, self]
@@ -236,14 +262,13 @@ module ActiveSupport
236
262
  # are treated as seconds.
237
263
  def +(other)
238
264
  if Duration === other
239
- parts = @parts.dup
240
- other.parts.each do |(key, value)|
241
- parts[key] += value
265
+ parts = @parts.merge(other._parts) do |_key, value, other_value|
266
+ value + other_value
242
267
  end
243
- Duration.new(value + other.value, parts)
268
+ Duration.new(value + other.value, parts, @variable || other.variable?)
244
269
  else
245
- seconds = @parts[:seconds] + other
246
- Duration.new(value + other, @parts.merge(seconds: seconds))
270
+ seconds = @parts.fetch(:seconds, 0) + other
271
+ Duration.new(value + other, @parts.merge(seconds: seconds), @variable)
247
272
  end
248
273
  end
249
274
 
@@ -256,9 +281,9 @@ module ActiveSupport
256
281
  # Multiplies this Duration by a Numeric and returns a new Duration.
257
282
  def *(other)
258
283
  if Scalar === other || Duration === other
259
- Duration.new(value * other.value, parts.map { |type, number| [type, number * other.value] })
284
+ Duration.new(value * other.value, @parts.transform_values { |number| number * other.value }, @variable || other.variable?)
260
285
  elsif Numeric === other
261
- Duration.new(value * other, parts.map { |type, number| [type, number * other] })
286
+ Duration.new(value * other, @parts.transform_values { |number| number * other }, @variable)
262
287
  else
263
288
  raise_type_error(other)
264
289
  end
@@ -267,11 +292,11 @@ module ActiveSupport
267
292
  # Divides this Duration by a Numeric and returns a new Duration.
268
293
  def /(other)
269
294
  if Scalar === other
270
- Duration.new(value / other.value, parts.map { |type, number| [type, number / other.value] })
295
+ Duration.new(value / other.value, @parts.transform_values { |number| number / other.value }, @variable)
271
296
  elsif Duration === other
272
297
  value / other.value
273
298
  elsif Numeric === other
274
- Duration.new(value / other, parts.map { |type, number| [type, number / other] })
299
+ Duration.new(value / other, @parts.transform_values { |number| number / other }, @variable)
275
300
  else
276
301
  raise_type_error(other)
277
302
  end
@@ -289,11 +314,15 @@ module ActiveSupport
289
314
  end
290
315
  end
291
316
 
292
- def -@ #:nodoc:
293
- Duration.new(-value, parts.map { |type, number| [type, -number] })
317
+ def -@ # :nodoc:
318
+ Duration.new(-value, @parts.transform_values(&:-@), @variable)
319
+ end
320
+
321
+ def +@ # :nodoc:
322
+ self
294
323
  end
295
324
 
296
- def is_a?(klass) #:nodoc:
325
+ def is_a?(klass) # :nodoc:
297
326
  Duration == klass || value.is_a?(klass)
298
327
  end
299
328
  alias :kind_of? :is_a?
@@ -343,6 +372,49 @@ module ActiveSupport
343
372
  def to_i
344
373
  @value.to_i
345
374
  end
375
+ alias :in_seconds :to_i
376
+
377
+ # Returns the amount of minutes a duration covers as a float
378
+ #
379
+ # 1.day.in_minutes # => 1440.0
380
+ def in_minutes
381
+ in_seconds / SECONDS_PER_MINUTE.to_f
382
+ end
383
+
384
+ # Returns the amount of hours a duration covers as a float
385
+ #
386
+ # 1.day.in_hours # => 24.0
387
+ def in_hours
388
+ in_seconds / SECONDS_PER_HOUR.to_f
389
+ end
390
+
391
+ # Returns the amount of days a duration covers as a float
392
+ #
393
+ # 12.hours.in_days # => 0.5
394
+ def in_days
395
+ in_seconds / SECONDS_PER_DAY.to_f
396
+ end
397
+
398
+ # Returns the amount of weeks a duration covers as a float
399
+ #
400
+ # 2.months.in_weeks # => 8.696
401
+ def in_weeks
402
+ in_seconds / SECONDS_PER_WEEK.to_f
403
+ end
404
+
405
+ # Returns the amount of months a duration covers as a float
406
+ #
407
+ # 9.weeks.in_months # => 2.07
408
+ def in_months
409
+ in_seconds / SECONDS_PER_MONTH.to_f
410
+ end
411
+
412
+ # Returns the amount of years a duration covers as a float
413
+ #
414
+ # 30.days.in_years # => 0.082
415
+ def in_years
416
+ in_seconds / SECONDS_PER_YEAR.to_f
417
+ end
346
418
 
347
419
  # Returns +true+ if +other+ is also a Duration instance, which has the
348
420
  # same parts as this one.
@@ -370,24 +442,24 @@ module ActiveSupport
370
442
  alias :until :ago
371
443
  alias :before :ago
372
444
 
373
- def inspect #:nodoc:
374
- return "#{value} seconds" if parts.empty?
445
+ def inspect # :nodoc:
446
+ return "#{value} seconds" if @parts.empty?
375
447
 
376
- parts.
448
+ @parts.
377
449
  sort_by { |unit, _ | PARTS.index(unit) }.
378
450
  map { |unit, val| "#{val} #{val == 1 ? unit.to_s.chop : unit.to_s}" }.
379
- to_sentence(locale: ::I18n.default_locale)
451
+ to_sentence(locale: false)
380
452
  end
381
453
 
382
- def as_json(options = nil) #:nodoc:
454
+ def as_json(options = nil) # :nodoc:
383
455
  to_i
384
456
  end
385
457
 
386
- def init_with(coder) #:nodoc:
458
+ def init_with(coder) # :nodoc:
387
459
  initialize(coder["value"], coder["parts"])
388
460
  end
389
461
 
390
- def encode_with(coder) #:nodoc:
462
+ def encode_with(coder) # :nodoc:
391
463
  coder.map = { "value" => @value, "parts" => @parts }
392
464
  end
393
465
 
@@ -397,16 +469,24 @@ module ActiveSupport
397
469
  ISO8601Serializer.new(self, precision: precision).serialize
398
470
  end
399
471
 
472
+ def variable? # :nodoc:
473
+ @variable
474
+ end
475
+
476
+ def _parts # :nodoc:
477
+ @parts
478
+ end
479
+
400
480
  private
401
481
  def sum(sign, time = ::Time.current)
402
482
  unless time.acts_like?(:time) || time.acts_like?(:date)
403
483
  raise ::ArgumentError, "expected a time or date, got #{time.inspect}"
404
484
  end
405
485
 
406
- if parts.empty?
486
+ if @parts.empty?
407
487
  time.since(sign * value)
408
488
  else
409
- parts.inject(time) do |t, (type, number)|
489
+ @parts.inject(time) do |t, (type, number)|
410
490
  if type == :seconds
411
491
  t.since(sign * number)
412
492
  elsif type == :minutes
@@ -4,42 +4,105 @@ require "yaml"
4
4
  require "active_support/encrypted_file"
5
5
  require "active_support/ordered_options"
6
6
  require "active_support/core_ext/object/inclusion"
7
+ require "active_support/core_ext/hash/keys"
7
8
  require "active_support/core_ext/module/delegation"
8
9
 
9
10
  module ActiveSupport
11
+ # = Encrypted Configuration
12
+ #
13
+ # Provides convenience methods on top of EncryptedFile to access values stored
14
+ # as encrypted YAML.
15
+ #
16
+ # Values can be accessed via +Hash+ methods, such as +fetch+ and +dig+, or via
17
+ # dynamic accessor methods, similar to OrderedOptions.
18
+ #
19
+ # my_config = ActiveSupport::EncryptedConfiguration.new(...)
20
+ # my_config.read # => "some_secret: 123\nsome_namespace:\n another_secret: 456"
21
+ #
22
+ # my_config[:some_secret]
23
+ # # => 123
24
+ # my_config.some_secret
25
+ # # => 123
26
+ # my_config.dig(:some_namespace, :another_secret)
27
+ # # => 456
28
+ # my_config.some_namespace.another_secret
29
+ # # => 456
30
+ # my_config.fetch(:foo)
31
+ # # => KeyError
32
+ # my_config.foo!
33
+ # # => KeyError
34
+ #
10
35
  class EncryptedConfiguration < EncryptedFile
11
- delegate :[], :fetch, to: :config
36
+ class InvalidContentError < RuntimeError
37
+ def initialize(content_path)
38
+ super "Invalid YAML in '#{content_path}'."
39
+ end
40
+
41
+ def message
42
+ cause.is_a?(Psych::SyntaxError) ? "#{super}\n\n #{cause.message}" : super
43
+ end
44
+ end
45
+
12
46
  delegate_missing_to :options
13
47
 
14
48
  def initialize(config_path:, key_path:, env_key:, raise_if_missing_key:)
15
49
  super content_path: config_path, key_path: key_path,
16
50
  env_key: env_key, raise_if_missing_key: raise_if_missing_key
51
+ @config = nil
52
+ @options = nil
17
53
  end
18
54
 
19
- # Allow a config to be started without a file present
55
+ # Reads the file and returns the decrypted content. See EncryptedFile#read.
20
56
  def read
21
57
  super
22
58
  rescue ActiveSupport::EncryptedFile::MissingContentError
59
+ # Allow a config to be started without a file present
23
60
  ""
24
61
  end
25
62
 
26
- def write(contents)
27
- deserialize(contents)
28
-
29
- super
63
+ def validate! # :nodoc:
64
+ deserialize(read)
30
65
  end
31
66
 
67
+ # Returns the decrypted content as a Hash with symbolized keys.
68
+ #
69
+ # my_config = ActiveSupport::EncryptedConfiguration.new(...)
70
+ # my_config.read # => "some_secret: 123\nsome_namespace:\n another_secret: 456"
71
+ #
72
+ # my_config.config
73
+ # # => { some_secret: 123, some_namespace: { another_secret: 789 } }
74
+ #
32
75
  def config
33
76
  @config ||= deserialize(read).deep_symbolize_keys
34
77
  end
35
78
 
79
+ def inspect # :nodoc:
80
+ "#<#{self.class.name}:#{'%#016x' % (object_id << 1)}>"
81
+ end
82
+
36
83
  private
84
+ def deep_transform(hash)
85
+ return hash unless hash.is_a?(Hash)
86
+
87
+ h = ActiveSupport::OrderedOptions.new
88
+ hash.each do |k, v|
89
+ h[k] = deep_transform(v)
90
+ end
91
+ h
92
+ end
93
+
37
94
  def options
38
- @options ||= ActiveSupport::InheritableOptions.new(config)
95
+ @options ||= deep_transform(config)
39
96
  end
40
97
 
41
- def deserialize(config)
42
- YAML.load(config).presence || {}
98
+ def deserialize(content)
99
+ config = YAML.respond_to?(:unsafe_load) ?
100
+ YAML.unsafe_load(content, filename: content_path) :
101
+ YAML.load(content, filename: content_path)
102
+
103
+ config.presence || {}
104
+ rescue Psych::SyntaxError
105
+ raise InvalidContentError.new(content_path)
43
106
  end
44
107
  end
45
108
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "pathname"
4
- require "tmpdir"
4
+ require "tempfile"
5
5
  require "active_support/message_encryptor"
6
6
 
7
7
  module ActiveSupport
@@ -20,24 +20,53 @@ 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
 
48
+ # Returns the encryption key, first trying the environment variable
49
+ # specified by +env_key+, then trying the key file specified by +key_path+.
50
+ # If +raise_if_missing_key+ is true, raises MissingKeyError if the
51
+ # environment variable is not set and the key file does not exist.
37
52
  def key
38
53
  read_env_key || read_key_file || handle_missing_key
39
54
  end
40
55
 
56
+ # Returns truthy if #key is truthy. Returns falsy otherwise. Unlike #key,
57
+ # does not raise MissingKeyError when +raise_if_missing_key+ is true.
58
+ def key?
59
+ read_env_key || read_key_file
60
+ end
61
+
62
+ # Reads the file and returns the decrypted content.
63
+ #
64
+ # Raises:
65
+ # - MissingKeyError if the key is missing and +raise_if_missing_key+ is true.
66
+ # - MissingContentError if the encrypted file does not exist or otherwise
67
+ # if the key is missing.
68
+ # - ActiveSupport::MessageEncryptor::InvalidMessage if the content cannot be
69
+ # decrypted or verified.
41
70
  def read
42
71
  if !key.nil? && content_path.exist?
43
72
  decrypt content_path.binread
@@ -58,21 +87,21 @@ module ActiveSupport
58
87
 
59
88
  private
60
89
  def writing(contents)
61
- tmp_file = "#{Process.pid}.#{content_path.basename.to_s.chomp('.enc')}"
62
- tmp_path = Pathname.new File.join(Dir.tmpdir, tmp_file)
63
- tmp_path.binwrite contents
90
+ Tempfile.create(["", "-" + content_path.basename.to_s.chomp(".enc")]) do |tmp_file|
91
+ tmp_path = Pathname.new(tmp_file)
92
+ tmp_path.binwrite contents
64
93
 
65
- yield tmp_path
94
+ yield tmp_path
66
95
 
67
- updated_contents = tmp_path.binread
96
+ updated_contents = tmp_path.binread
68
97
 
69
- write(updated_contents) if updated_contents != contents
70
- ensure
71
- FileUtils.rm(tmp_path) if tmp_path&.exist?
98
+ write(updated_contents) if updated_contents != contents
99
+ end
72
100
  end
73
101
 
74
102
 
75
103
  def encrypt(contents)
104
+ check_key_length
76
105
  encryptor.encrypt_and_sign contents
77
106
  end
78
107
 
@@ -81,20 +110,24 @@ module ActiveSupport
81
110
  end
82
111
 
83
112
  def encryptor
84
- @encryptor ||= ActiveSupport::MessageEncryptor.new([ key ].pack("H*"), cipher: CIPHER)
113
+ @encryptor ||= ActiveSupport::MessageEncryptor.new([ key ].pack("H*"), cipher: CIPHER, serializer: Marshal)
85
114
  end
86
115
 
87
116
 
88
117
  def read_env_key
89
- ENV[env_key]
118
+ ENV[env_key].presence
90
119
  end
91
120
 
92
121
  def read_key_file
93
- key_path.binread.strip if key_path.exist?
122
+ @key_file_contents ||= (key_path.binread.strip if key_path.exist?)
94
123
  end
95
124
 
96
125
  def handle_missing_key
97
126
  raise MissingKeyError.new(key_path: key_path, env_key: env_key) if raise_if_missing_key
98
127
  end
128
+
129
+ def check_key_length
130
+ raise InvalidKeyLengthError if key&.length != self.class.expected_key_length
131
+ end
99
132
  end
100
133
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/string_inquirer"
4
+ require "active_support/core_ext/object/inclusion"
5
+
6
+ module ActiveSupport
7
+ class EnvironmentInquirer < StringInquirer # :nodoc:
8
+ # Optimization for the three default environments, so this inquirer doesn't need to rely on
9
+ # the slower delegation through method_missing that StringInquirer would normally entail.
10
+ DEFAULT_ENVIRONMENTS = %w[ development test production ]
11
+
12
+ # Environments that'll respond true for #local?
13
+ LOCAL_ENVIRONMENTS = %w[ development test ]
14
+
15
+ def initialize(env)
16
+ raise(ArgumentError, "'local' is a reserved environment name") if env == "local"
17
+
18
+ super(env)
19
+
20
+ DEFAULT_ENVIRONMENTS.each do |default|
21
+ instance_variable_set :"@#{default}", env == default
22
+ end
23
+
24
+ @local = in? LOCAL_ENVIRONMENTS
25
+ end
26
+
27
+ DEFAULT_ENVIRONMENTS.each do |env|
28
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
29
+ def #{env}?
30
+ @#{env}
31
+ end
32
+ RUBY
33
+ end
34
+
35
+ # Returns true if we're in the development or test environment.
36
+ def local?
37
+ @local
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport::ErrorReporter::TestHelper # :nodoc:
4
+ class ErrorSubscriber
5
+ attr_reader :events
6
+
7
+ def initialize
8
+ @events = []
9
+ end
10
+
11
+ def report(error, handled:, severity:, source:, context:)
12
+ @events << [error, handled, severity, source, context]
13
+ end
14
+ end
15
+ end