activesupport 7.0.8.7 → 7.1.5.1

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 (181) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +995 -294
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +4 -4
  5. data/lib/active_support/actionable_error.rb +3 -1
  6. data/lib/active_support/array_inquirer.rb +2 -0
  7. data/lib/active_support/backtrace_cleaner.rb +30 -5
  8. data/lib/active_support/benchmarkable.rb +1 -0
  9. data/lib/active_support/broadcast_logger.rb +251 -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 +37 -10
  14. data/lib/active_support/cache/mem_cache_store.rb +100 -76
  15. data/lib/active_support/cache/memory_store.rb +78 -24
  16. data/lib/active_support/cache/null_store.rb +6 -0
  17. data/lib/active_support/cache/redis_cache_store.rb +151 -141
  18. data/lib/active_support/cache/serializer_with_fallback.rb +175 -0
  19. data/lib/active_support/cache/strategy/local_cache.rb +29 -14
  20. data/lib/active_support/cache.rb +333 -253
  21. data/lib/active_support/callbacks.rb +44 -21
  22. data/lib/active_support/code_generator.rb +15 -10
  23. data/lib/active_support/concern.rb +4 -2
  24. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +42 -3
  25. data/lib/active_support/concurrency/null_lock.rb +13 -0
  26. data/lib/active_support/configurable.rb +10 -0
  27. data/lib/active_support/core_ext/array/conversions.rb +2 -1
  28. data/lib/active_support/core_ext/array.rb +0 -1
  29. data/lib/active_support/core_ext/class/subclasses.rb +13 -10
  30. data/lib/active_support/core_ext/date/conversions.rb +2 -1
  31. data/lib/active_support/core_ext/date.rb +0 -1
  32. data/lib/active_support/core_ext/date_and_time/calculations.rb +10 -0
  33. data/lib/active_support/core_ext/date_time/conversions.rb +6 -2
  34. data/lib/active_support/core_ext/date_time.rb +0 -1
  35. data/lib/active_support/core_ext/digest/uuid.rb +1 -10
  36. data/lib/active_support/core_ext/enumerable.rb +3 -75
  37. data/lib/active_support/core_ext/erb/util.rb +196 -0
  38. data/lib/active_support/core_ext/hash/conversions.rb +1 -1
  39. data/lib/active_support/core_ext/hash/deep_merge.rb +22 -14
  40. data/lib/active_support/core_ext/module/attribute_accessors.rb +6 -0
  41. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +34 -16
  42. data/lib/active_support/core_ext/module/concerning.rb +6 -6
  43. data/lib/active_support/core_ext/module/delegation.rb +81 -37
  44. data/lib/active_support/core_ext/module/deprecation.rb +15 -12
  45. data/lib/active_support/core_ext/module/introspection.rb +0 -1
  46. data/lib/active_support/core_ext/numeric/bytes.rb +9 -0
  47. data/lib/active_support/core_ext/numeric/conversions.rb +2 -0
  48. data/lib/active_support/core_ext/numeric.rb +0 -1
  49. data/lib/active_support/core_ext/object/deep_dup.rb +16 -0
  50. data/lib/active_support/core_ext/object/inclusion.rb +13 -5
  51. data/lib/active_support/core_ext/object/instance_variables.rb +22 -12
  52. data/lib/active_support/core_ext/object/json.rb +16 -6
  53. data/lib/active_support/core_ext/object/with.rb +44 -0
  54. data/lib/active_support/core_ext/object/with_options.rb +4 -4
  55. data/lib/active_support/core_ext/object.rb +1 -0
  56. data/lib/active_support/core_ext/pathname/blank.rb +16 -0
  57. data/lib/active_support/core_ext/pathname/existence.rb +2 -0
  58. data/lib/active_support/core_ext/pathname.rb +1 -0
  59. data/lib/active_support/core_ext/range/conversions.rb +28 -7
  60. data/lib/active_support/core_ext/range/overlap.rb +40 -0
  61. data/lib/active_support/core_ext/range.rb +1 -2
  62. data/lib/active_support/core_ext/securerandom.rb +24 -12
  63. data/lib/active_support/core_ext/string/filters.rb +20 -14
  64. data/lib/active_support/core_ext/string/indent.rb +1 -1
  65. data/lib/active_support/core_ext/string/inflections.rb +16 -5
  66. data/lib/active_support/core_ext/string/output_safety.rb +38 -174
  67. data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
  68. data/lib/active_support/core_ext/time/calculations.rb +18 -2
  69. data/lib/active_support/core_ext/time/conversions.rb +2 -2
  70. data/lib/active_support/core_ext/time/zones.rb +4 -4
  71. data/lib/active_support/core_ext/time.rb +0 -1
  72. data/lib/active_support/current_attributes.rb +15 -6
  73. data/lib/active_support/deep_mergeable.rb +53 -0
  74. data/lib/active_support/dependencies/autoload.rb +17 -12
  75. data/lib/active_support/deprecation/behaviors.rb +65 -42
  76. data/lib/active_support/deprecation/constant_accessor.rb +5 -4
  77. data/lib/active_support/deprecation/deprecators.rb +104 -0
  78. data/lib/active_support/deprecation/disallowed.rb +3 -5
  79. data/lib/active_support/deprecation/instance_delegator.rb +31 -4
  80. data/lib/active_support/deprecation/method_wrappers.rb +6 -23
  81. data/lib/active_support/deprecation/proxy_wrappers.rb +37 -22
  82. data/lib/active_support/deprecation/reporting.rb +43 -26
  83. data/lib/active_support/deprecation.rb +32 -5
  84. data/lib/active_support/deprecator.rb +7 -0
  85. data/lib/active_support/descendants_tracker.rb +104 -132
  86. data/lib/active_support/duration/iso8601_serializer.rb +0 -2
  87. data/lib/active_support/duration.rb +2 -1
  88. data/lib/active_support/encrypted_configuration.rb +30 -9
  89. data/lib/active_support/encrypted_file.rb +8 -3
  90. data/lib/active_support/environment_inquirer.rb +22 -2
  91. data/lib/active_support/error_reporter/test_helper.rb +15 -0
  92. data/lib/active_support/error_reporter.rb +121 -35
  93. data/lib/active_support/execution_wrapper.rb +4 -4
  94. data/lib/active_support/file_update_checker.rb +4 -2
  95. data/lib/active_support/fork_tracker.rb +10 -2
  96. data/lib/active_support/gem_version.rb +4 -4
  97. data/lib/active_support/gzip.rb +2 -0
  98. data/lib/active_support/hash_with_indifferent_access.rb +35 -17
  99. data/lib/active_support/html_safe_translation.rb +16 -6
  100. data/lib/active_support/i18n.rb +1 -1
  101. data/lib/active_support/i18n_railtie.rb +20 -13
  102. data/lib/active_support/inflector/inflections.rb +2 -0
  103. data/lib/active_support/inflector/methods.rb +23 -11
  104. data/lib/active_support/inflector/transliterate.rb +3 -1
  105. data/lib/active_support/isolated_execution_state.rb +26 -22
  106. data/lib/active_support/json/decoding.rb +2 -1
  107. data/lib/active_support/json/encoding.rb +25 -43
  108. data/lib/active_support/key_generator.rb +9 -1
  109. data/lib/active_support/lazy_load_hooks.rb +6 -4
  110. data/lib/active_support/locale/en.yml +2 -0
  111. data/lib/active_support/log_subscriber.rb +85 -33
  112. data/lib/active_support/logger.rb +9 -60
  113. data/lib/active_support/logger_thread_safe_level.rb +10 -24
  114. data/lib/active_support/message_encryptor.rb +197 -53
  115. data/lib/active_support/message_encryptors.rb +141 -0
  116. data/lib/active_support/message_pack/cache_serializer.rb +23 -0
  117. data/lib/active_support/message_pack/extensions.rb +292 -0
  118. data/lib/active_support/message_pack/serializer.rb +63 -0
  119. data/lib/active_support/message_pack.rb +50 -0
  120. data/lib/active_support/message_verifier.rb +212 -93
  121. data/lib/active_support/message_verifiers.rb +135 -0
  122. data/lib/active_support/messages/codec.rb +65 -0
  123. data/lib/active_support/messages/metadata.rb +111 -45
  124. data/lib/active_support/messages/rotation_coordinator.rb +93 -0
  125. data/lib/active_support/messages/rotator.rb +34 -32
  126. data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
  127. data/lib/active_support/multibyte/chars.rb +2 -0
  128. data/lib/active_support/multibyte/unicode.rb +9 -37
  129. data/lib/active_support/notifications/fanout.rb +245 -81
  130. data/lib/active_support/notifications/instrumenter.rb +87 -22
  131. data/lib/active_support/notifications.rb +1 -1
  132. data/lib/active_support/number_helper/number_converter.rb +14 -5
  133. data/lib/active_support/number_helper/number_to_currency_converter.rb +6 -6
  134. data/lib/active_support/number_helper/number_to_human_size_converter.rb +3 -3
  135. data/lib/active_support/number_helper/number_to_phone_converter.rb +1 -0
  136. data/lib/active_support/number_helper.rb +379 -318
  137. data/lib/active_support/ordered_hash.rb +3 -3
  138. data/lib/active_support/ordered_options.rb +14 -0
  139. data/lib/active_support/parameter_filter.rb +84 -69
  140. data/lib/active_support/proxy_object.rb +2 -0
  141. data/lib/active_support/railtie.rb +33 -21
  142. data/lib/active_support/reloader.rb +12 -4
  143. data/lib/active_support/rescuable.rb +2 -0
  144. data/lib/active_support/secure_compare_rotator.rb +16 -9
  145. data/lib/active_support/string_inquirer.rb +3 -1
  146. data/lib/active_support/subscriber.rb +9 -27
  147. data/lib/active_support/syntax_error_proxy.rb +60 -0
  148. data/lib/active_support/tagged_logging.rb +64 -24
  149. data/lib/active_support/test_case.rb +153 -6
  150. data/lib/active_support/testing/assertions.rb +26 -10
  151. data/lib/active_support/testing/autorun.rb +0 -2
  152. data/lib/active_support/testing/constant_stubbing.rb +32 -0
  153. data/lib/active_support/testing/deprecation.rb +25 -25
  154. data/lib/active_support/testing/error_reporter_assertions.rb +107 -0
  155. data/lib/active_support/testing/isolation.rb +1 -1
  156. data/lib/active_support/testing/method_call_assertions.rb +21 -8
  157. data/lib/active_support/testing/parallelize_executor.rb +8 -3
  158. data/lib/active_support/testing/setup_and_teardown.rb +2 -0
  159. data/lib/active_support/testing/stream.rb +1 -1
  160. data/lib/active_support/testing/strict_warnings.rb +39 -0
  161. data/lib/active_support/testing/time_helpers.rb +37 -15
  162. data/lib/active_support/time_with_zone.rb +4 -14
  163. data/lib/active_support/values/time_zone.rb +18 -7
  164. data/lib/active_support/version.rb +1 -1
  165. data/lib/active_support/xml_mini/jdom.rb +3 -10
  166. data/lib/active_support/xml_mini/nokogiri.rb +1 -1
  167. data/lib/active_support/xml_mini/nokogirisax.rb +1 -1
  168. data/lib/active_support/xml_mini/rexml.rb +1 -1
  169. data/lib/active_support/xml_mini.rb +2 -2
  170. data/lib/active_support.rb +14 -3
  171. metadata +143 -14
  172. data/lib/active_support/core_ext/array/deprecated_conversions.rb +0 -25
  173. data/lib/active_support/core_ext/date/deprecated_conversions.rb +0 -40
  174. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +0 -36
  175. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +0 -60
  176. data/lib/active_support/core_ext/range/deprecated_conversions.rb +0 -36
  177. data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -5
  178. data/lib/active_support/core_ext/range/overlaps.rb +0 -10
  179. data/lib/active_support/core_ext/time/deprecated_conversions.rb +0 -73
  180. data/lib/active_support/core_ext/uri.rb +0 -5
  181. data/lib/active_support/per_thread_registry.rb +0 -65
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+
5
+ module ActiveSupport
6
+ module CoreExt
7
+ module ERBUtil
8
+ # HTML escapes strings but doesn't wrap them with an ActiveSupport::SafeBuffer.
9
+ # This method is not for public consumption! Seriously!
10
+ def html_escape(s) # :nodoc:
11
+ s = s.to_s
12
+ if s.html_safe?
13
+ s
14
+ else
15
+ super(ActiveSupport::Multibyte::Unicode.tidy_bytes(s))
16
+ end
17
+ end
18
+ alias :unwrapped_html_escape :html_escape # :nodoc:
19
+
20
+ # A utility method for escaping HTML tag characters.
21
+ # This method is also aliased as <tt>h</tt>.
22
+ #
23
+ # puts html_escape('is a > 0 & a < 10?')
24
+ # # => is a &gt; 0 &amp; a &lt; 10?
25
+ def html_escape(s) # rubocop:disable Lint/DuplicateMethods
26
+ unwrapped_html_escape(s).html_safe
27
+ end
28
+ alias h html_escape
29
+ end
30
+
31
+ module ERBUtilPrivate
32
+ include ERBUtil
33
+ private :unwrapped_html_escape, :html_escape, :h
34
+ end
35
+ end
36
+ end
37
+
38
+ class ERB
39
+ module Util
40
+ HTML_ESCAPE = { "&" => "&amp;", ">" => "&gt;", "<" => "&lt;", '"' => "&quot;", "'" => "&#39;" }
41
+ HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+)|(#[xX][\dA-Fa-f]+));)/
42
+
43
+ # Following XML requirements: https://www.w3.org/TR/REC-xml/#NT-Name
44
+ TAG_NAME_START_CODEPOINTS = "@:A-Z_a-z\u{C0}-\u{D6}\u{D8}-\u{F6}\u{F8}-\u{2FF}\u{370}-\u{37D}\u{37F}-\u{1FFF}" \
45
+ "\u{200C}-\u{200D}\u{2070}-\u{218F}\u{2C00}-\u{2FEF}\u{3001}-\u{D7FF}\u{F900}-\u{FDCF}" \
46
+ "\u{FDF0}-\u{FFFD}\u{10000}-\u{EFFFF}"
47
+ INVALID_TAG_NAME_START_REGEXP = /[^#{TAG_NAME_START_CODEPOINTS}]/
48
+ TAG_NAME_FOLLOWING_CODEPOINTS = "#{TAG_NAME_START_CODEPOINTS}\\-.0-9\u{B7}\u{0300}-\u{036F}\u{203F}-\u{2040}"
49
+ INVALID_TAG_NAME_FOLLOWING_REGEXP = /[^#{TAG_NAME_FOLLOWING_CODEPOINTS}]/
50
+ SAFE_XML_TAG_NAME_REGEXP = /\A[#{TAG_NAME_START_CODEPOINTS}][#{TAG_NAME_FOLLOWING_CODEPOINTS}]*\z/
51
+ TAG_NAME_REPLACEMENT_CHAR = "_"
52
+
53
+ prepend ActiveSupport::CoreExt::ERBUtilPrivate
54
+ singleton_class.prepend ActiveSupport::CoreExt::ERBUtil
55
+
56
+ # A utility method for escaping HTML without affecting existing escaped entities.
57
+ #
58
+ # html_escape_once('1 < 2 &amp; 3')
59
+ # # => "1 &lt; 2 &amp; 3"
60
+ #
61
+ # html_escape_once('&lt;&lt; Accept & Checkout')
62
+ # # => "&lt;&lt; Accept &amp; Checkout"
63
+ def html_escape_once(s)
64
+ ActiveSupport::Multibyte::Unicode.tidy_bytes(s.to_s).gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE).html_safe
65
+ end
66
+
67
+ module_function :html_escape_once
68
+
69
+ # A utility method for escaping HTML entities in JSON strings. Specifically, the
70
+ # &, > and < characters are replaced with their equivalent unicode escaped form -
71
+ # \u0026, \u003e, and \u003c. The Unicode sequences \u2028 and \u2029 are also
72
+ # escaped as they are treated as newline characters in some JavaScript engines.
73
+ # These sequences have identical meaning as the original characters inside the
74
+ # context of a JSON string, so assuming the input is a valid and well-formed
75
+ # JSON value, the output will have equivalent meaning when parsed:
76
+ #
77
+ # json = JSON.generate({ name: "</script><script>alert('PWNED!!!')</script>"})
78
+ # # => "{\"name\":\"</script><script>alert('PWNED!!!')</script>\"}"
79
+ #
80
+ # json_escape(json)
81
+ # # => "{\"name\":\"\\u003C/script\\u003E\\u003Cscript\\u003Ealert('PWNED!!!')\\u003C/script\\u003E\"}"
82
+ #
83
+ # JSON.parse(json) == JSON.parse(json_escape(json))
84
+ # # => true
85
+ #
86
+ # The intended use case for this method is to escape JSON strings before including
87
+ # them inside a script tag to avoid XSS vulnerability:
88
+ #
89
+ # <script>
90
+ # var currentUser = <%= raw json_escape(current_user.to_json) %>;
91
+ # </script>
92
+ #
93
+ # It is necessary to +raw+ the result of +json_escape+, so that quotation marks
94
+ # don't get converted to <tt>&quot;</tt> entities. +json_escape+ doesn't
95
+ # automatically flag the result as HTML safe, since the raw value is unsafe to
96
+ # use inside HTML attributes.
97
+ #
98
+ # If your JSON is being used downstream for insertion into the DOM, be aware of
99
+ # whether or not it is being inserted via <tt>html()</tt>. Most jQuery plugins do this.
100
+ # If that is the case, be sure to +html_escape+ or +sanitize+ any user-generated
101
+ # content returned by your JSON.
102
+ #
103
+ # If you need to output JSON elsewhere in your HTML, you can just do something
104
+ # like this, as any unsafe characters (including quotation marks) will be
105
+ # automatically escaped for you:
106
+ #
107
+ # <div data-user-info="<%= current_user.to_json %>">...</div>
108
+ #
109
+ # WARNING: this helper only works with valid JSON. Using this on non-JSON values
110
+ # will open up serious XSS vulnerabilities. For example, if you replace the
111
+ # +current_user.to_json+ in the example above with user input instead, the browser
112
+ # will happily <tt>eval()</tt> that string as JavaScript.
113
+ #
114
+ # The escaping performed in this method is identical to those performed in the
115
+ # Active Support JSON encoder when +ActiveSupport.escape_html_entities_in_json+ is
116
+ # set to true. Because this transformation is idempotent, this helper can be
117
+ # applied even if +ActiveSupport.escape_html_entities_in_json+ is already true.
118
+ #
119
+ # Therefore, when you are unsure if +ActiveSupport.escape_html_entities_in_json+
120
+ # is enabled, or if you are unsure where your JSON string originated from, it
121
+ # is recommended that you always apply this helper (other libraries, such as the
122
+ # JSON gem, do not provide this kind of protection by default; also some gems
123
+ # might override +to_json+ to bypass Active Support's encoder).
124
+ def json_escape(s)
125
+ result = s.to_s.dup
126
+ result.gsub!(">", '\u003e')
127
+ result.gsub!("<", '\u003c')
128
+ result.gsub!("&", '\u0026')
129
+ result.gsub!("\u2028", '\u2028')
130
+ result.gsub!("\u2029", '\u2029')
131
+ s.html_safe? ? result.html_safe : result
132
+ end
133
+
134
+ module_function :json_escape
135
+
136
+ # A utility method for escaping XML names of tags and names of attributes.
137
+ #
138
+ # xml_name_escape('1 < 2 & 3')
139
+ # # => "1___2___3"
140
+ #
141
+ # It follows the requirements of the specification: https://www.w3.org/TR/REC-xml/#NT-Name
142
+ def xml_name_escape(name)
143
+ name = name.to_s
144
+ return "" if name.blank?
145
+ return name if name.match?(SAFE_XML_TAG_NAME_REGEXP)
146
+
147
+ starting_char = name[0]
148
+ starting_char.gsub!(INVALID_TAG_NAME_START_REGEXP, TAG_NAME_REPLACEMENT_CHAR)
149
+
150
+ return starting_char if name.size == 1
151
+
152
+ following_chars = name[1..-1]
153
+ following_chars.gsub!(INVALID_TAG_NAME_FOLLOWING_REGEXP, TAG_NAME_REPLACEMENT_CHAR)
154
+
155
+ starting_char << following_chars
156
+ end
157
+ module_function :xml_name_escape
158
+
159
+ # Tokenizes a line of ERB. This is really just for error reporting and
160
+ # nobody should use it.
161
+ def self.tokenize(source) # :nodoc:
162
+ require "strscan"
163
+ source = StringScanner.new(source.chomp)
164
+ tokens = []
165
+
166
+ start_re = /<%(?:={1,2}|-|\#|%)?/m
167
+ finish_re = /(?:[-=])?%>/m
168
+
169
+ while !source.eos?
170
+ pos = source.pos
171
+ source.scan_until(/(?:#{start_re}|#{finish_re})/)
172
+ raise NotImplementedError if source.matched.nil?
173
+ len = source.pos - source.matched.bytesize - pos
174
+
175
+ case source.matched
176
+ when start_re
177
+ tokens << [:TEXT, source.string[pos, len]] if len > 0
178
+ tokens << [:OPEN, source.matched]
179
+ if source.scan(/(.*?)(?=#{finish_re}|\z)/m)
180
+ tokens << [:CODE, source.matched] unless source.matched.empty?
181
+ tokens << [:CLOSE, source.scan(finish_re)] unless source.eos?
182
+ else
183
+ raise NotImplementedError
184
+ end
185
+ when finish_re
186
+ tokens << [:CODE, source.string[pos, len]] if len > 0
187
+ tokens << [:CLOSE, source.matched]
188
+ else
189
+ raise NotImplementedError, source.matched
190
+ end
191
+ end
192
+
193
+ tokens
194
+ end
195
+ end
196
+ end
@@ -68,7 +68,7 @@ class Hash
68
68
  #
69
69
  # By default the root node is "hash", but that's configurable via the <tt>:root</tt> option.
70
70
  #
71
- # The default XML builder is a fresh instance of <tt>Builder::XmlMarkup</tt>. You can
71
+ # The default XML builder is a fresh instance of +Builder::XmlMarkup+. You can
72
72
  # configure your own builder with the <tt>:builder</tt> option. The method also accepts
73
73
  # options like <tt>:dasherize</tt> and friends, they are forwarded to the builder.
74
74
  def to_xml(options = {})
@@ -1,6 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/deep_mergeable"
4
+
3
5
  class Hash
6
+ include ActiveSupport::DeepMergeable
7
+
8
+ ##
9
+ # :method: deep_merge
10
+ # :call-seq: deep_merge(other_hash, &block)
11
+ #
4
12
  # Returns a new hash with +self+ and +other_hash+ merged recursively.
5
13
  #
6
14
  # h1 = { a: true, b: { c: [1, 2, 3] } }
@@ -15,20 +23,20 @@ class Hash
15
23
  # h2 = { b: 250, c: { c1: 200 } }
16
24
  # h1.deep_merge(h2) { |key, this_val, other_val| this_val + other_val }
17
25
  # # => { a: 100, b: 450, c: { c1: 300 } }
18
- def deep_merge(other_hash, &block)
19
- dup.deep_merge!(other_hash, &block)
20
- end
26
+ #
27
+ #--
28
+ # Implemented by ActiveSupport::DeepMergeable#deep_merge.
29
+
30
+ ##
31
+ # :method: deep_merge!
32
+ # :call-seq: deep_merge!(other_hash, &block)
33
+ #
34
+ # Same as #deep_merge, but modifies +self+.
35
+ #
36
+ #--
37
+ # Implemented by ActiveSupport::DeepMergeable#deep_merge!.
21
38
 
22
- # Same as +deep_merge+, but modifies +self+.
23
- def deep_merge!(other_hash, &block)
24
- merge!(other_hash) do |key, this_val, other_val|
25
- if this_val.is_a?(Hash) && other_val.is_a?(Hash)
26
- this_val.deep_merge(other_val, &block)
27
- elsif block_given?
28
- block.call(key, this_val, other_val)
29
- else
30
- other_val
31
- end
32
- end
39
+ def deep_merge?(other) # :nodoc:
40
+ other.is_a?(Hash)
33
41
  end
34
42
  end
@@ -43,6 +43,7 @@ class Module
43
43
  #
44
44
  # module HairColors
45
45
  # mattr_reader :hair_colors, default: [:brown, :black, :blonde, :red]
46
+ # mattr_reader(:hair_styles) { [:long, :short] }
46
47
  # end
47
48
  #
48
49
  # class Person
@@ -50,6 +51,7 @@ class Module
50
51
  # end
51
52
  #
52
53
  # Person.new.hair_colors # => [:brown, :black, :blonde, :red]
54
+ # Person.new.hair_styles # => [:long, :short]
53
55
  def mattr_reader(*syms, instance_reader: true, instance_accessor: true, default: nil, location: nil)
54
56
  raise TypeError, "module attributes should be defined directly on class, not singleton" if singleton_class?
55
57
  location ||= caller_locations(1, 1).first
@@ -107,6 +109,7 @@ class Module
107
109
  #
108
110
  # module HairColors
109
111
  # mattr_writer :hair_colors, default: [:brown, :black, :blonde, :red]
112
+ # mattr_writer(:hair_styles) { [:long, :short] }
110
113
  # end
111
114
  #
112
115
  # class Person
@@ -114,6 +117,7 @@ class Module
114
117
  # end
115
118
  #
116
119
  # Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red]
120
+ # Person.class_variable_get("@@hair_styles") # => [:long, :short]
117
121
  def mattr_writer(*syms, instance_writer: true, instance_accessor: true, default: nil, location: nil)
118
122
  raise TypeError, "module attributes should be defined directly on class, not singleton" if singleton_class?
119
123
  location ||= caller_locations(1, 1).first
@@ -192,6 +196,7 @@ class Module
192
196
  #
193
197
  # module HairColors
194
198
  # mattr_accessor :hair_colors, default: [:brown, :black, :blonde, :red]
199
+ # mattr_accessor(:hair_styles) { [:long, :short] }
195
200
  # end
196
201
  #
197
202
  # class Person
@@ -199,6 +204,7 @@ class Module
199
204
  # end
200
205
  #
201
206
  # Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red]
207
+ # Person.class_variable_get("@@hair_styles") # => [:long, :short]
202
208
  def mattr_accessor(*syms, instance_reader: true, instance_writer: true, instance_accessor: true, default: nil, &blk)
203
209
  location = caller_locations(1, 1).first
204
210
  mattr_reader(*syms, instance_reader: instance_reader, instance_accessor: instance_accessor, default: default, location: location, &blk)
@@ -42,14 +42,32 @@ class Module
42
42
  syms.each do |sym|
43
43
  raise NameError.new("invalid attribute name: #{sym}") unless /^[_A-Za-z]\w*$/.match?(sym)
44
44
 
45
- # The following generated method concatenates `name` because we want it
46
- # to work with inheritance via polymorphism.
47
- class_eval(<<-EOS, __FILE__, __LINE__ + 1)
48
- def self.#{sym}
49
- @__thread_mattr_#{sym} ||= "attr_\#{name}_#{sym}"
50
- ::ActiveSupport::IsolatedExecutionState[@__thread_mattr_#{sym}]
51
- end
52
- EOS
45
+ # The following generated method concatenates `object_id` because we want
46
+ # subclasses to maintain independent values.
47
+ if default.nil?
48
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
49
+ def self.#{sym}
50
+ @__thread_mattr_#{sym} ||= "attr_#{sym}_\#{object_id}"
51
+ ::ActiveSupport::IsolatedExecutionState[@__thread_mattr_#{sym}]
52
+ end
53
+ EOS
54
+ else
55
+ default = default.dup.freeze unless default.frozen?
56
+ singleton_class.define_method("#{sym}_default_value") { default }
57
+
58
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
59
+ def self.#{sym}
60
+ @__thread_mattr_#{sym} ||= "attr_#{sym}_\#{object_id}"
61
+ value = ::ActiveSupport::IsolatedExecutionState[@__thread_mattr_#{sym}]
62
+
63
+ if value.nil? && !::ActiveSupport::IsolatedExecutionState.key?(@__thread_mattr_#{sym})
64
+ ::ActiveSupport::IsolatedExecutionState[@__thread_mattr_#{sym}] = #{sym}_default_value
65
+ else
66
+ value
67
+ end
68
+ end
69
+ EOS
70
+ end
53
71
 
54
72
  if instance_reader && instance_accessor
55
73
  class_eval(<<-EOS, __FILE__, __LINE__ + 1)
@@ -58,8 +76,6 @@ class Module
58
76
  end
59
77
  EOS
60
78
  end
61
-
62
- ::ActiveSupport::IsolatedExecutionState["attr_#{name}_#{sym}"] = default unless default.nil?
63
79
  end
64
80
  end
65
81
  alias :thread_cattr_reader :thread_mattr_reader
@@ -82,15 +98,15 @@ class Module
82
98
  # end
83
99
  #
84
100
  # Current.new.user = "DHH" # => NoMethodError
85
- def thread_mattr_writer(*syms, instance_writer: true, instance_accessor: true, default: nil) # :nodoc:
101
+ def thread_mattr_writer(*syms, instance_writer: true, instance_accessor: true) # :nodoc:
86
102
  syms.each do |sym|
87
103
  raise NameError.new("invalid attribute name: #{sym}") unless /^[_A-Za-z]\w*$/.match?(sym)
88
104
 
89
- # The following generated method concatenates `name` because we want it
90
- # to work with inheritance via polymorphism.
105
+ # The following generated method concatenates `object_id` because we want
106
+ # subclasses to maintain independent values.
91
107
  class_eval(<<-EOS, __FILE__, __LINE__ + 1)
92
108
  def self.#{sym}=(obj)
93
- @__thread_mattr_#{sym} ||= "attr_\#{name}_#{sym}"
109
+ @__thread_mattr_#{sym} ||= "attr_#{sym}_\#{object_id}"
94
110
  ::ActiveSupport::IsolatedExecutionState[@__thread_mattr_#{sym}] = obj
95
111
  end
96
112
  EOS
@@ -102,8 +118,6 @@ class Module
102
118
  end
103
119
  EOS
104
120
  end
105
-
106
- public_send("#{sym}=", default) unless default.nil?
107
121
  end
108
122
  end
109
123
  alias :thread_cattr_writer :thread_mattr_writer
@@ -149,6 +163,10 @@ class Module
149
163
  #
150
164
  # Current.new.user = "DHH" # => NoMethodError
151
165
  # Current.new.user # => NoMethodError
166
+ #
167
+ # A default value may be specified using the +:default+ option. Because
168
+ # multiple threads can access the default value, non-frozen default values
169
+ # will be <tt>dup</tt>ed and frozen.
152
170
  def thread_mattr_accessor(*syms, instance_reader: true, instance_writer: true, instance_accessor: true, default: nil)
153
171
  thread_mattr_reader(*syms, instance_reader: instance_reader, instance_accessor: instance_accessor, default: default)
154
172
  thread_mattr_writer(*syms, instance_writer: instance_writer, instance_accessor: instance_accessor)
@@ -3,7 +3,7 @@
3
3
  require "active_support/concern"
4
4
 
5
5
  class Module
6
- # = Bite-sized separation of concerns
6
+ # == Bite-sized separation of concerns
7
7
  #
8
8
  # We often find ourselves with a medium-sized chunk of behavior that we'd
9
9
  # like to extract, but only mix in to a single class.
@@ -18,9 +18,9 @@ class Module
18
18
  # with a comment, as a least-bad alternative. Using modules in separate files
19
19
  # means tedious sifting to get a big-picture view.
20
20
  #
21
- # = Dissatisfying ways to separate small concerns
21
+ # == Dissatisfying ways to separate small concerns
22
22
  #
23
- # == Using comments:
23
+ # === Using comments:
24
24
  #
25
25
  # class Todo < ApplicationRecord
26
26
  # # Other todo implementation
@@ -37,7 +37,7 @@ class Module
37
37
  # end
38
38
  # end
39
39
  #
40
- # == With an inline module:
40
+ # === With an inline module:
41
41
  #
42
42
  # Noisy syntax.
43
43
  #
@@ -61,7 +61,7 @@ class Module
61
61
  # include EventTracking
62
62
  # end
63
63
  #
64
- # == Mix-in noise exiled to its own file:
64
+ # === Mix-in noise exiled to its own file:
65
65
  #
66
66
  # Once our chunk of behavior starts pushing the scroll-to-understand-it
67
67
  # boundary, we give in and move it to a separate file. At this size, the
@@ -75,7 +75,7 @@ class Module
75
75
  # include TodoEventTracking
76
76
  # end
77
77
  #
78
- # = Introducing Module#concerning
78
+ # == Introducing Module#concerning
79
79
  #
80
80
  # By quieting the mix-in noise, we arrive at a natural, low-ceremony way to
81
81
  # separate bite-sized concerns.
@@ -7,9 +7,9 @@ class Module
7
7
  # option is not used.
8
8
  class DelegationError < NoMethodError; end
9
9
 
10
- RUBY_RESERVED_KEYWORDS = %w(alias and BEGIN begin break case class def defined? do
11
- else elsif END end ensure false for if in module next nil not or redo rescue retry
12
- return self super then true undef unless until when while yield)
10
+ RUBY_RESERVED_KEYWORDS = %w(__ENCODING__ __LINE__ __FILE__ alias and BEGIN begin break
11
+ case class def defined? do else elsif END end ensure false for if in module next nil
12
+ not or redo rescue retry return self super then true undef unless until when while yield)
13
13
  DELEGATION_RESERVED_KEYWORDS = %w(_ arg args block)
14
14
  DELEGATION_RESERVED_METHOD_NAMES = Set.new(
15
15
  RUBY_RESERVED_KEYWORDS + DELEGATION_RESERVED_KEYWORDS
@@ -187,19 +187,49 @@ class Module
187
187
  location = caller_locations(1, 1).first
188
188
  file, line = location.path, location.lineno
189
189
 
190
- to = to.to_s
191
- to = "self.#{to}" if DELEGATION_RESERVED_METHOD_NAMES.include?(to)
190
+ receiver = to.to_s
191
+ receiver = "self.#{receiver}" if DELEGATION_RESERVED_METHOD_NAMES.include?(receiver)
192
192
 
193
193
  method_def = []
194
194
  method_names = []
195
195
 
196
- methods.map do |method|
196
+ method_def << "self.private" if private
197
+
198
+ methods.each do |method|
197
199
  method_name = prefix ? "#{method_prefix}#{method}" : method
198
200
  method_names << method_name.to_sym
199
201
 
200
202
  # Attribute writer methods only accept one argument. Makes sure []=
201
203
  # methods still accept two arguments.
202
- definition = /[^\]]=\z/.match?(method) ? "arg" : "..."
204
+ definition = \
205
+ if /[^\]]=\z/.match?(method)
206
+ "arg"
207
+ else
208
+ method_object =
209
+ begin
210
+ if to.is_a?(Module)
211
+ to.method(method)
212
+ elsif receiver == "self.class"
213
+ method(method)
214
+ end
215
+ rescue NameError
216
+ # Do nothing. Fall back to `"..."`
217
+ end
218
+
219
+ if method_object
220
+ parameters = method_object.parameters
221
+
222
+ if (parameters.map(&:first) & [:opt, :rest, :keyreq, :key, :keyrest]).any?
223
+ "..."
224
+ else
225
+ defn = parameters.filter_map { |type, arg| arg if type == :req }
226
+ defn << "&block"
227
+ defn.join(", ")
228
+ end
229
+ else
230
+ "..."
231
+ end
232
+ end
203
233
 
204
234
  # The following generated method calls the target exactly once, storing
205
235
  # the returned value in a dummy variable.
@@ -213,7 +243,7 @@ class Module
213
243
 
214
244
  method_def <<
215
245
  "def #{method_name}(#{definition})" <<
216
- " _ = #{to}" <<
246
+ " _ = #{receiver}" <<
217
247
  " if !_.nil? || nil.respond_to?(:#{method})" <<
218
248
  " _.#{method}(#{definition})" <<
219
249
  " end" <<
@@ -224,11 +254,11 @@ class Module
224
254
 
225
255
  method_def <<
226
256
  "def #{method_name}(#{definition})" <<
227
- " _ = #{to}" <<
257
+ " _ = #{receiver}" <<
228
258
  " _.#{method}(#{definition})" <<
229
259
  "rescue NoMethodError => e" <<
230
260
  " if _.nil? && e.name == :#{method}" <<
231
- %( raise DelegationError, "#{self}##{method_name} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}") <<
261
+ %( raise DelegationError, "#{self}##{method_name} delegated to #{receiver}.#{method}, but #{receiver} is nil: \#{self.inspect}") <<
232
262
  " else" <<
233
263
  " raise" <<
234
264
  " end" <<
@@ -236,7 +266,6 @@ class Module
236
266
  end
237
267
  end
238
268
  module_eval(method_def.join(";"), file, line)
239
- private(*method_names) if private
240
269
  method_names
241
270
  end
242
271
 
@@ -288,37 +317,52 @@ class Module
288
317
  # of <tt>object</tt> add or remove instance variables.
289
318
  def delegate_missing_to(target, allow_nil: nil)
290
319
  target = target.to_s
291
- target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target)
320
+ target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target) || target == "__target"
292
321
 
293
- module_eval <<-RUBY, __FILE__, __LINE__ + 1
294
- def respond_to_missing?(name, include_private = false)
295
- # It may look like an oversight, but we deliberately do not pass
296
- # +include_private+, because they do not get delegated.
322
+ if allow_nil
323
+ module_eval <<~RUBY, __FILE__, __LINE__ + 1
324
+ def respond_to_missing?(name, include_private = false)
325
+ # It may look like an oversight, but we deliberately do not pass
326
+ # +include_private+, because they do not get delegated.
297
327
 
298
- return false if name == :marshal_dump || name == :_dump
299
- #{target}.respond_to?(name) || super
300
- end
328
+ return false if name == :marshal_dump || name == :_dump
329
+ #{target}.respond_to?(name) || super
330
+ end
301
331
 
302
- def method_missing(method, *args, &block)
303
- if #{target}.respond_to?(method)
304
- #{target}.public_send(method, *args, &block)
305
- else
306
- begin
332
+ def method_missing(method, *args, &block)
333
+ __target = #{target}
334
+ if __target.nil? && !nil.respond_to?(method)
335
+ nil
336
+ elsif __target.respond_to?(method)
337
+ __target.public_send(method, *args, &block)
338
+ else
307
339
  super
308
- rescue NoMethodError
309
- if #{target}.nil?
310
- if #{allow_nil == true}
311
- nil
312
- else
313
- raise DelegationError, "\#{method} delegated to #{target}, but #{target} is nil"
314
- end
315
- else
316
- raise
317
- end
318
340
  end
319
341
  end
320
- end
321
- ruby2_keywords(:method_missing)
322
- RUBY
342
+ ruby2_keywords(:method_missing)
343
+ RUBY
344
+ else
345
+ module_eval <<~RUBY, __FILE__, __LINE__ + 1
346
+ def respond_to_missing?(name, include_private = false)
347
+ # It may look like an oversight, but we deliberately do not pass
348
+ # +include_private+, because they do not get delegated.
349
+
350
+ return false if name == :marshal_dump || name == :_dump
351
+ #{target}.respond_to?(name) || super
352
+ end
353
+
354
+ def method_missing(method, *args, &block)
355
+ __target = #{target}
356
+ if __target.nil? && !nil.respond_to?(method)
357
+ raise DelegationError, "\#{method} delegated to #{target}, but #{target} is nil"
358
+ elsif __target.respond_to?(method)
359
+ __target.public_send(method, *args, &block)
360
+ else
361
+ super
362
+ end
363
+ end
364
+ ruby2_keywords(:method_missing)
365
+ RUBY
366
+ end
323
367
  end
324
368
  end
@@ -1,17 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Module
4
- # deprecate :foo
5
- # deprecate bar: 'message'
6
- # deprecate :foo, :bar, baz: 'warning!', qux: 'gone!'
4
+ # deprecate :foo, deprecator: MyLib.deprecator
5
+ # deprecate :foo, bar: "warning!", deprecator: MyLib.deprecator
7
6
  #
8
- # You can also use custom deprecator instance:
9
- #
10
- # deprecate :foo, deprecator: MyLib::Deprecator.new
11
- # deprecate :foo, bar: "warning!", deprecator: MyLib::Deprecator.new
12
- #
13
- # \Custom deprecators must respond to <tt>deprecation_warning(deprecated_method_name, message, caller_backtrace)</tt>
14
- # method where you can implement your custom warning behavior.
7
+ # A deprecator is typically an instance of ActiveSupport::Deprecation, but you can also pass any object that responds
8
+ # to <tt>deprecation_warning(deprecated_method_name, message, caller_backtrace)</tt> where you can implement your
9
+ # custom warning behavior.
15
10
  #
16
11
  # class MyLib::Deprecator
17
12
  # def deprecation_warning(deprecated_method_name, message, caller_backtrace = nil)
@@ -19,7 +14,15 @@ class Module
19
14
  # Kernel.warn message
20
15
  # end
21
16
  # end
22
- def deprecate(*method_names)
23
- ActiveSupport::Deprecation.deprecate_methods(self, *method_names)
17
+ def deprecate(*method_names, deprecator: nil, **options)
18
+ if deprecator.is_a?(ActiveSupport::Deprecation)
19
+ deprecator.deprecate_methods(self, *method_names, **options)
20
+ elsif deprecator
21
+ # we just need any instance to call deprecate_methods, but the deprecation will be emitted by deprecator
22
+ ActiveSupport.deprecator.deprecate_methods(self, *method_names, **options, deprecator: deprecator)
23
+ else
24
+ ActiveSupport.deprecator.warn("Module.deprecate without a deprecator is deprecated")
25
+ ActiveSupport::Deprecation._instance.deprecate_methods(self, *method_names, **options)
26
+ end
24
27
  end
25
28
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/string/filters"
4
3
  require "active_support/inflector"
5
4
 
6
5
  class Module