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
@@ -1,80 +1,146 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "time"
4
+ require "active_support/json"
5
+ require_relative "serializer_with_fallback"
4
6
 
5
7
  module ActiveSupport
6
- module Messages #:nodoc:
7
- class Metadata #:nodoc:
8
- def initialize(message, expires_at = nil, purpose = nil)
9
- @message, @purpose = message, purpose
10
- @expires_at = expires_at.is_a?(String) ? parse_expires_at(expires_at) : expires_at
11
- end
8
+ module Messages # :nodoc:
9
+ module Metadata # :nodoc:
10
+ singleton_class.attr_accessor :use_message_serializer_for_metadata
11
+
12
+ ENVELOPE_SERIALIZERS = [
13
+ *SerializerWithFallback::SERIALIZERS.values,
14
+ ActiveSupport::JSON,
15
+ ::JSON,
16
+ Marshal,
17
+ ]
18
+
19
+ TIMESTAMP_SERIALIZERS = [
20
+ SerializerWithFallback::SERIALIZERS.fetch(:message_pack),
21
+ SerializerWithFallback::SERIALIZERS.fetch(:message_pack_allow_marshal),
22
+ ]
12
23
 
13
- def as_json(options = {})
14
- { _rails: { message: @message, exp: @expires_at, pur: @purpose } }
24
+ ActiveSupport.on_load(:message_pack) do
25
+ ENVELOPE_SERIALIZERS << ActiveSupport::MessagePack
26
+ TIMESTAMP_SERIALIZERS << ActiveSupport::MessagePack
15
27
  end
16
28
 
17
- class << self
18
- def wrap(message, expires_at: nil, expires_in: nil, purpose: nil)
19
- if expires_at || expires_in || purpose
20
- JSON.encode new(encode(message), pick_expiry(expires_at, expires_in), purpose)
29
+ private
30
+ def serialize_with_metadata(data, **metadata)
31
+ has_metadata = metadata.any? { |k, v| v }
32
+
33
+ if has_metadata && !use_message_serializer_for_metadata?
34
+ data_string = serialize_to_json_safe_string(data)
35
+ envelope = wrap_in_metadata_legacy_envelope({ "message" => data_string }, **metadata)
36
+ serialize_to_json(envelope)
21
37
  else
22
- message
38
+ data = wrap_in_metadata_envelope({ "data" => data }, **metadata) if has_metadata
39
+ serialize(data)
23
40
  end
24
41
  end
25
42
 
26
- def verify(message, purpose)
27
- extract_metadata(message).verify(purpose)
28
- end
29
-
30
- private
31
- def pick_expiry(expires_at, expires_in)
32
- if expires_at
33
- expires_at.utc.iso8601(3)
34
- elsif expires_in
35
- Time.now.utc.advance(seconds: expires_in).iso8601(3)
43
+ def deserialize_with_metadata(message, **expected_metadata)
44
+ if dual_serialized_metadata_envelope_json?(message)
45
+ envelope = deserialize_from_json(message)
46
+ extracted = extract_from_metadata_envelope(envelope, **expected_metadata)
47
+ deserialize_from_json_safe_string(extracted["message"])
48
+ else
49
+ deserialized = deserialize(message)
50
+ if metadata_envelope?(deserialized)
51
+ extract_from_metadata_envelope(deserialized, **expected_metadata)["data"]
52
+ elsif expected_metadata.none? { |k, v| v }
53
+ deserialized
54
+ else
55
+ throw :invalid_message_content, "missing metadata"
36
56
  end
37
57
  end
58
+ end
38
59
 
39
- def extract_metadata(message)
40
- data = JSON.decode(message) rescue nil
60
+ def use_message_serializer_for_metadata?
61
+ Metadata.use_message_serializer_for_metadata && Metadata::ENVELOPE_SERIALIZERS.include?(serializer)
62
+ end
41
63
 
42
- if data.is_a?(Hash) && data.key?("_rails")
43
- new(decode(data["_rails"]["message"]), data["_rails"]["exp"], data["_rails"]["pur"])
44
- else
45
- new(message)
46
- end
47
- end
64
+ def wrap_in_metadata_envelope(hash, expires_at: nil, expires_in: nil, purpose: nil)
65
+ expiry = pick_expiry(expires_at, expires_in)
66
+ hash["exp"] = expiry if expiry
67
+ hash["pur"] = purpose.to_s if purpose
68
+ { "_rails" => hash }
69
+ end
70
+
71
+ def wrap_in_metadata_legacy_envelope(hash, expires_at: nil, expires_in: nil, purpose: nil)
72
+ expiry = pick_expiry(expires_at, expires_in)
73
+ hash["exp"] = expiry
74
+ hash["pur"] = purpose
75
+ { "_rails" => hash }
76
+ end
77
+
78
+ def extract_from_metadata_envelope(envelope, purpose: nil)
79
+ hash = envelope["_rails"]
48
80
 
49
- def encode(message)
50
- ::Base64.strict_encode64(message)
81
+ if hash["exp"] && Time.now.utc >= parse_expiry(hash["exp"])
82
+ throw :invalid_message_content, "expired"
51
83
  end
52
84
 
53
- def decode(message)
54
- ::Base64.strict_decode64(message)
85
+ if hash["pur"].to_s != purpose.to_s
86
+ throw :invalid_message_content, "mismatched purpose"
55
87
  end
56
- end
57
88
 
58
- def verify(purpose)
59
- @message if match?(purpose) && fresh?
60
- end
89
+ hash
90
+ end
61
91
 
62
- private
63
- def match?(purpose)
64
- @purpose.to_s == purpose.to_s
92
+ def metadata_envelope?(object)
93
+ object.is_a?(Hash) && object.key?("_rails")
65
94
  end
66
95
 
67
- def fresh?
68
- @expires_at.nil? || Time.now.utc < @expires_at
96
+ def dual_serialized_metadata_envelope_json?(string)
97
+ string.start_with?('{"_rails":{"message":')
98
+ end
99
+
100
+ def pick_expiry(expires_at, expires_in)
101
+ expiry = if expires_at
102
+ expires_at.utc
103
+ elsif expires_in
104
+ Time.now.utc.advance(seconds: expires_in)
105
+ end
106
+
107
+ unless Metadata::TIMESTAMP_SERIALIZERS.include?(serializer)
108
+ expiry = expiry&.iso8601(3)
109
+ end
110
+
111
+ expiry
69
112
  end
70
113
 
71
- def parse_expires_at(expires_at)
72
- if ActiveSupport.use_standard_json_time_format
114
+ def parse_expiry(expires_at)
115
+ if !expires_at.is_a?(String)
116
+ expires_at
117
+ elsif ActiveSupport.use_standard_json_time_format
73
118
  Time.iso8601(expires_at)
74
119
  else
75
120
  Time.parse(expires_at)
76
121
  end
77
122
  end
123
+
124
+ def serialize_to_json(data)
125
+ ActiveSupport::JSON.encode(data)
126
+ end
127
+
128
+ def deserialize_from_json(serialized)
129
+ ActiveSupport::JSON.decode(serialized)
130
+ rescue ::JSON::ParserError => error
131
+ # Throw :invalid_message_format instead of :invalid_message_serialization
132
+ # because here a parse error is due to a bad message rather than an
133
+ # incompatible `self.serializer`.
134
+ throw :invalid_message_format, error
135
+ end
136
+
137
+ def serialize_to_json_safe_string(data)
138
+ encode(serialize(data), url_safe: false)
139
+ end
140
+
141
+ def deserialize_from_json_safe_string(string)
142
+ deserialize(decode(string, url_safe: false))
143
+ end
78
144
  end
79
145
  end
80
146
  end
@@ -9,7 +9,8 @@ module ActiveSupport
9
9
  @signed, @encrypted = [], []
10
10
  end
11
11
 
12
- def rotate(kind, *args)
12
+ def rotate(kind, *args, **options)
13
+ args << options unless options.empty?
13
14
  case kind
14
15
  when :signed
15
16
  @signed << args
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/slice"
4
+
5
+ module ActiveSupport
6
+ module Messages
7
+ class RotationCoordinator # :nodoc:
8
+ attr_accessor :transitional
9
+
10
+ def initialize(&secret_generator)
11
+ raise ArgumentError, "A secret generator block is required" unless secret_generator
12
+ @secret_generator = secret_generator
13
+ @rotate_options = []
14
+ @on_rotation = nil
15
+ @codecs = {}
16
+ end
17
+
18
+ def [](salt)
19
+ @codecs[salt] ||= build_with_rotations(salt)
20
+ end
21
+
22
+ def []=(salt, codec)
23
+ @codecs[salt] = codec
24
+ end
25
+
26
+ def rotate(**options, &block)
27
+ raise ArgumentError, "Options cannot be specified when using a block" if block && !options.empty?
28
+ changing_configuration!
29
+
30
+ @rotate_options << (block || options)
31
+
32
+ self
33
+ end
34
+
35
+ def rotate_defaults
36
+ rotate()
37
+ end
38
+
39
+ def clear_rotations
40
+ changing_configuration!
41
+ @rotate_options.clear
42
+ self
43
+ end
44
+
45
+ def on_rotation(&callback)
46
+ changing_configuration!
47
+ @on_rotation = callback
48
+ end
49
+
50
+ private
51
+ def changing_configuration!
52
+ if @codecs.any?
53
+ raise <<~MESSAGE
54
+ Cannot change #{self.class} configuration after it has already been applied.
55
+
56
+ The configuration has been applied with the following salts:
57
+ #{@codecs.keys.map { |salt| "- #{salt.inspect}" }.join("\n")}
58
+ MESSAGE
59
+ end
60
+ end
61
+
62
+ def normalize_options(options)
63
+ options = options.dup
64
+
65
+ options[:secret_generator] ||= @secret_generator
66
+
67
+ secret_generator_kwargs = options[:secret_generator].parameters.
68
+ filter_map { |type, name| name if type == :key || type == :keyreq }
69
+ options[:secret_generator_options] = options.extract!(*secret_generator_kwargs)
70
+
71
+ options[:on_rotation] = @on_rotation
72
+
73
+ options
74
+ end
75
+
76
+ def build_with_rotations(salt)
77
+ rotate_options = @rotate_options.map { |options| options.is_a?(Proc) ? options.(salt) : options }
78
+ transitional = self.transitional && rotate_options.first
79
+ rotate_options.compact!
80
+ rotate_options[0..1] = rotate_options[0..1].reverse if transitional
81
+ rotate_options = rotate_options.map { |options| normalize_options(options) }.uniq
82
+
83
+ raise "No options have been configured for #{salt}" if rotate_options.empty?
84
+
85
+ rotate_options.map { |options| build(salt.to_s, **options) }.reduce(&:fall_back_to)
86
+ end
87
+
88
+ def build(salt, secret_generator:, secret_generator_options:, **options)
89
+ raise NotImplementedError
90
+ end
91
+ end
92
+ end
93
+ end
@@ -3,53 +3,56 @@
3
3
  module ActiveSupport
4
4
  module Messages
5
5
  module Rotator # :nodoc:
6
- def initialize(*, **options)
7
- super
8
-
9
- @options = options
6
+ def initialize(*args, on_rotation: nil, **options)
7
+ super(*args, **options)
8
+ @args = args
9
+ @options = options
10
10
  @rotations = []
11
+ @on_rotation = on_rotation
11
12
  end
12
13
 
13
- def rotate(*secrets, **options)
14
- @rotations << build_rotation(*secrets, @options.merge(options))
14
+ def rotate(*args, **options)
15
+ fall_back_to build_rotation(*args, **options)
15
16
  end
16
17
 
17
- module Encryptor
18
- include Rotator
19
-
20
- def decrypt_and_verify(*args, on_rotation: nil, **options)
21
- super
22
- rescue MessageEncryptor::InvalidMessage, MessageVerifier::InvalidSignature
23
- run_rotations(on_rotation) { |encryptor| encryptor.decrypt_and_verify(*args, **options) } || raise
24
- end
18
+ def fall_back_to(fallback)
19
+ @rotations << fallback
20
+ self
21
+ end
25
22
 
26
- private
27
- def build_rotation(secret = @secret, sign_secret = @sign_secret, options)
28
- self.class.new(secret, sign_secret, **options)
23
+ def read_message(message, on_rotation: @on_rotation, **options)
24
+ if @rotations.empty?
25
+ super(message, **options)
26
+ else
27
+ thrown, error = catch_rotation_error do
28
+ return super(message, **options)
29
29
  end
30
- end
31
30
 
32
- module Verifier
33
- include Rotator
31
+ @rotations.each do |rotation|
32
+ catch_rotation_error do
33
+ value = rotation.read_message(message, **options)
34
+ on_rotation&.call
35
+ return value
36
+ end
37
+ end
34
38
 
35
- def verified(*args, on_rotation: nil, **options)
36
- super || run_rotations(on_rotation) { |verifier| verifier.verified(*args, **options) }
39
+ throw thrown, error
37
40
  end
38
-
39
- private
40
- def build_rotation(secret = @secret, options)
41
- self.class.new(secret, **options)
42
- end
43
41
  end
44
42
 
45
43
  private
46
- def run_rotations(on_rotation)
47
- @rotations.find do |rotation|
48
- if message = yield(rotation) rescue next
49
- on_rotation.call if on_rotation
50
- return message
44
+ def build_rotation(*args, **options)
45
+ self.class.new(*args, *@args.drop(args.length), **@options, **options)
46
+ end
47
+
48
+ def catch_rotation_error(&block)
49
+ error = catch :invalid_message_format do
50
+ error = catch :invalid_message_serialization do
51
+ return [nil, block.call]
51
52
  end
53
+ return [:invalid_message_serialization, error]
52
54
  end
55
+ [:invalid_message_format, error]
53
56
  end
54
57
  end
55
58
  end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/kernel/reporting"
4
+ require "active_support/notifications"
5
+
6
+ module ActiveSupport
7
+ module Messages # :nodoc:
8
+ module SerializerWithFallback # :nodoc:
9
+ def self.[](format)
10
+ if format.to_s.include?("message_pack") && !defined?(ActiveSupport::MessagePack)
11
+ require "active_support/message_pack"
12
+ end
13
+
14
+ SERIALIZERS.fetch(format)
15
+ end
16
+
17
+ def load(dumped)
18
+ format = detect_format(dumped)
19
+
20
+ if format == self.format
21
+ _load(dumped)
22
+ elsif format && fallback?(format)
23
+ payload = { serializer: SERIALIZERS.key(self), fallback: format, serialized: dumped }
24
+ ActiveSupport::Notifications.instrument("message_serializer_fallback.active_support", payload) do
25
+ payload[:deserialized] = SERIALIZERS[format]._load(dumped)
26
+ end
27
+ else
28
+ raise "Unsupported serialization format"
29
+ end
30
+ end
31
+
32
+ private
33
+ def detect_format(dumped)
34
+ case
35
+ when MessagePackWithFallback.dumped?(dumped)
36
+ :message_pack
37
+ when MarshalWithFallback.dumped?(dumped)
38
+ :marshal
39
+ when JsonWithFallback.dumped?(dumped)
40
+ :json
41
+ end
42
+ end
43
+
44
+ def fallback?(format)
45
+ format != :marshal
46
+ end
47
+
48
+ module AllowMarshal
49
+ private
50
+ def fallback?(format)
51
+ super || format == :marshal
52
+ end
53
+ end
54
+
55
+ module MarshalWithFallback
56
+ include SerializerWithFallback
57
+ extend self
58
+
59
+ def format
60
+ :marshal
61
+ end
62
+
63
+ def dump(object)
64
+ Marshal.dump(object)
65
+ end
66
+
67
+ def _load(dumped)
68
+ Marshal.load(dumped)
69
+ end
70
+
71
+ MARSHAL_SIGNATURE = "\x04\x08"
72
+
73
+ def dumped?(dumped)
74
+ dumped.start_with?(MARSHAL_SIGNATURE)
75
+ end
76
+ end
77
+
78
+ module JsonWithFallback
79
+ include SerializerWithFallback
80
+ extend self
81
+
82
+ def format
83
+ :json
84
+ end
85
+
86
+ def dump(object)
87
+ ActiveSupport::JSON.encode(object)
88
+ end
89
+
90
+ def _load(dumped)
91
+ ActiveSupport::JSON.decode(dumped)
92
+ end
93
+
94
+ JSON_START_WITH = /\A(?:[{\["]|-?\d|true|false|null)/
95
+
96
+ def dumped?(dumped)
97
+ JSON_START_WITH.match?(dumped)
98
+ end
99
+
100
+ private
101
+ def detect_format(dumped)
102
+ # Assume JSON format if format could not be determined.
103
+ super || :json
104
+ end
105
+ end
106
+
107
+ module JsonWithFallbackAllowMarshal
108
+ include JsonWithFallback
109
+ include AllowMarshal
110
+ extend self
111
+ end
112
+
113
+ module MessagePackWithFallback
114
+ include SerializerWithFallback
115
+ extend self
116
+
117
+ def format
118
+ :message_pack
119
+ end
120
+
121
+ def dump(object)
122
+ ActiveSupport::MessagePack.dump(object)
123
+ end
124
+
125
+ def _load(dumped)
126
+ ActiveSupport::MessagePack.load(dumped)
127
+ end
128
+
129
+ def dumped?(dumped)
130
+ available? && ActiveSupport::MessagePack.signature?(dumped)
131
+ end
132
+
133
+ private
134
+ def available?
135
+ return @available if defined?(@available)
136
+ silence_warnings { require "active_support/message_pack" }
137
+ @available = true
138
+ rescue LoadError
139
+ @available = false
140
+ end
141
+ end
142
+
143
+ module MessagePackWithFallbackAllowMarshal
144
+ include MessagePackWithFallback
145
+ include AllowMarshal
146
+ extend self
147
+ end
148
+
149
+ SERIALIZERS = {
150
+ marshal: MarshalWithFallback,
151
+ json: JsonWithFallback,
152
+ json_allow_marshal: JsonWithFallbackAllowMarshal,
153
+ message_pack: MessagePackWithFallback,
154
+ message_pack_allow_marshal: MessagePackWithFallbackAllowMarshal,
155
+ }
156
+ end
157
+ end
158
+ end
@@ -5,8 +5,10 @@ require "active_support/core_ext/string/access"
5
5
  require "active_support/core_ext/string/behavior"
6
6
  require "active_support/core_ext/module/delegation"
7
7
 
8
- module ActiveSupport #:nodoc:
9
- module Multibyte #:nodoc:
8
+ module ActiveSupport # :nodoc:
9
+ module Multibyte # :nodoc:
10
+ # = Active Support \Multibyte \Chars
11
+ #
10
12
  # Chars enables you to work transparently with UTF-8 encoding in the Ruby
11
13
  # String class without having extensive knowledge about the encoding. A
12
14
  # Chars object accepts a string upon initialization and proxies String
@@ -48,7 +50,7 @@ module ActiveSupport #:nodoc:
48
50
  alias to_s wrapped_string
49
51
  alias to_str wrapped_string
50
52
 
51
- delegate :<=>, :=~, :acts_like_string?, to: :wrapped_string
53
+ delegate :<=>, :=~, :match?, :acts_like_string?, to: :wrapped_string
52
54
 
53
55
  # Creates a new Chars instance by wrapping _string_.
54
56
  def initialize(string)
@@ -59,7 +61,7 @@ module ActiveSupport #:nodoc:
59
61
  # Forward all undefined methods to the wrapped string.
60
62
  def method_missing(method, *args, &block)
61
63
  result = @wrapped_string.__send__(method, *args, &block)
62
- if /!$/.match?(method)
64
+ if method.end_with?("!")
63
65
  self if result
64
66
  else
65
67
  result.kind_of?(String) ? chars(result) : result
@@ -73,17 +75,6 @@ module ActiveSupport #:nodoc:
73
75
  @wrapped_string.respond_to?(method, include_private)
74
76
  end
75
77
 
76
- # Returns +true+ when the proxy class can handle the string. Returns
77
- # +false+ otherwise.
78
- def self.consumes?(string)
79
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
80
- ActiveSupport::Multibyte::Chars.consumes? is deprecated and will be
81
- removed from Rails 6.1. Use string.is_utf8? instead.
82
- MSG
83
-
84
- string.encoding == Encoding::UTF_8
85
- end
86
-
87
78
  # Works just like <tt>String#split</tt>, with the exception that the items
88
79
  # in the resulting list are Chars instances instead of String. This makes
89
80
  # chaining methods easier.
@@ -113,7 +104,7 @@ module ActiveSupport #:nodoc:
113
104
  #
114
105
  # 'Café'.mb_chars.reverse.to_s # => 'éfaC'
115
106
  def reverse
116
- chars(@wrapped_string.scan(/\X/).reverse.join)
107
+ chars(@wrapped_string.grapheme_clusters.reverse.join)
117
108
  end
118
109
 
119
110
  # Limits the byte size of the string to a number of bytes without breaking
@@ -134,46 +125,18 @@ module ActiveSupport #:nodoc:
134
125
  end
135
126
  alias_method :titlecase, :titleize
136
127
 
137
- # Returns the KC normalization of the string by default. NFKC is
138
- # considered the best normalization form for passing strings to databases
139
- # and validations.
140
- #
141
- # * <tt>form</tt> - The form you want to normalize in. Should be one of the following:
142
- # <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>. Default is
143
- # ActiveSupport::Multibyte::Unicode.default_normalization_form
144
- def normalize(form = nil)
145
- form ||= Unicode.default_normalization_form
146
-
147
- # See https://www.unicode.org/reports/tr15, Table 1
148
- if alias_form = Unicode::NORMALIZATION_FORM_ALIASES[form]
149
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
150
- ActiveSupport::Multibyte::Chars#normalize is deprecated and will be
151
- removed from Rails 6.1. Use #unicode_normalize(:#{alias_form}) instead.
152
- MSG
153
-
154
- send(:unicode_normalize, alias_form)
155
- else
156
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
157
- ActiveSupport::Multibyte::Chars#normalize is deprecated and will be
158
- removed from Rails 6.1. Use #unicode_normalize instead.
159
- MSG
160
-
161
- raise ArgumentError, "#{form} is not a valid normalization variant", caller
162
- end
163
- end
164
-
165
128
  # Performs canonical decomposition on all the characters.
166
129
  #
167
- # 'é'.length # => 2
168
- # 'é'.mb_chars.decompose.to_s.length # => 3
130
+ # 'é'.length # => 1
131
+ # 'é'.mb_chars.decompose.to_s.length # => 2
169
132
  def decompose
170
133
  chars(Unicode.decompose(:canonical, @wrapped_string.codepoints.to_a).pack("U*"))
171
134
  end
172
135
 
173
136
  # Performs composition on all the characters.
174
137
  #
175
- # 'é'.length # => 3
176
- # 'é'.mb_chars.compose.to_s.length # => 2
138
+ # 'é'.length # => 1
139
+ # 'é'.mb_chars.compose.to_s.length # => 1
177
140
  def compose
178
141
  chars(Unicode.compose(@wrapped_string.codepoints.to_a).pack("U*"))
179
142
  end
@@ -181,9 +144,9 @@ module ActiveSupport #:nodoc:
181
144
  # Returns the number of grapheme clusters in the string.
182
145
  #
183
146
  # 'क्षि'.mb_chars.length # => 4
184
- # 'क्षि'.mb_chars.grapheme_length # => 3
147
+ # 'क्षि'.mb_chars.grapheme_length # => 2
185
148
  def grapheme_length
186
- @wrapped_string.scan(/\X/).length
149
+ @wrapped_string.grapheme_clusters.length
187
150
  end
188
151
 
189
152
  # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent
@@ -195,13 +158,13 @@ module ActiveSupport #:nodoc:
195
158
  chars(Unicode.tidy_bytes(@wrapped_string, force))
196
159
  end
197
160
 
198
- def as_json(options = nil) #:nodoc:
161
+ def as_json(options = nil) # :nodoc:
199
162
  to_s.as_json(options)
200
163
  end
201
164
 
202
165
  %w(reverse tidy_bytes).each do |method|
203
166
  define_method("#{method}!") do |*args|
204
- @wrapped_string = send(method, *args).to_s
167
+ @wrapped_string = public_send(method, *args).to_s
205
168
  self
206
169
  end
207
170
  end