activesupport 6.0.6.1 → 7.1.3.2

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