activesupport 6.0.0

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 (250) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +572 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +40 -0
  5. data/lib/active_support.rb +96 -0
  6. data/lib/active_support/actionable_error.rb +48 -0
  7. data/lib/active_support/all.rb +5 -0
  8. data/lib/active_support/array_inquirer.rb +48 -0
  9. data/lib/active_support/backtrace_cleaner.rb +132 -0
  10. data/lib/active_support/benchmarkable.rb +51 -0
  11. data/lib/active_support/builder.rb +8 -0
  12. data/lib/active_support/cache.rb +830 -0
  13. data/lib/active_support/cache/file_store.rb +196 -0
  14. data/lib/active_support/cache/mem_cache_store.rb +212 -0
  15. data/lib/active_support/cache/memory_store.rb +174 -0
  16. data/lib/active_support/cache/null_store.rb +48 -0
  17. data/lib/active_support/cache/redis_cache_store.rb +488 -0
  18. data/lib/active_support/cache/strategy/local_cache.rb +194 -0
  19. data/lib/active_support/cache/strategy/local_cache_middleware.rb +45 -0
  20. data/lib/active_support/callbacks.rb +856 -0
  21. data/lib/active_support/concern.rb +171 -0
  22. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +17 -0
  23. data/lib/active_support/concurrency/share_lock.rb +227 -0
  24. data/lib/active_support/configurable.rb +146 -0
  25. data/lib/active_support/core_ext.rb +5 -0
  26. data/lib/active_support/core_ext/array.rb +9 -0
  27. data/lib/active_support/core_ext/array/access.rb +104 -0
  28. data/lib/active_support/core_ext/array/conversions.rb +213 -0
  29. data/lib/active_support/core_ext/array/extract.rb +21 -0
  30. data/lib/active_support/core_ext/array/extract_options.rb +31 -0
  31. data/lib/active_support/core_ext/array/grouping.rb +109 -0
  32. data/lib/active_support/core_ext/array/inquiry.rb +19 -0
  33. data/lib/active_support/core_ext/array/prepend_and_append.rb +5 -0
  34. data/lib/active_support/core_ext/array/wrap.rb +48 -0
  35. data/lib/active_support/core_ext/benchmark.rb +16 -0
  36. data/lib/active_support/core_ext/big_decimal.rb +3 -0
  37. data/lib/active_support/core_ext/big_decimal/conversions.rb +14 -0
  38. data/lib/active_support/core_ext/class.rb +4 -0
  39. data/lib/active_support/core_ext/class/attribute.rb +141 -0
  40. data/lib/active_support/core_ext/class/attribute_accessors.rb +6 -0
  41. data/lib/active_support/core_ext/class/subclasses.rb +54 -0
  42. data/lib/active_support/core_ext/date.rb +7 -0
  43. data/lib/active_support/core_ext/date/acts_like.rb +10 -0
  44. data/lib/active_support/core_ext/date/blank.rb +14 -0
  45. data/lib/active_support/core_ext/date/calculations.rb +146 -0
  46. data/lib/active_support/core_ext/date/conversions.rb +96 -0
  47. data/lib/active_support/core_ext/date/zones.rb +8 -0
  48. data/lib/active_support/core_ext/date_and_time/calculations.rb +351 -0
  49. data/lib/active_support/core_ext/date_and_time/compatibility.rb +16 -0
  50. data/lib/active_support/core_ext/date_and_time/zones.rb +41 -0
  51. data/lib/active_support/core_ext/date_time.rb +7 -0
  52. data/lib/active_support/core_ext/date_time/acts_like.rb +16 -0
  53. data/lib/active_support/core_ext/date_time/blank.rb +14 -0
  54. data/lib/active_support/core_ext/date_time/calculations.rb +211 -0
  55. data/lib/active_support/core_ext/date_time/compatibility.rb +18 -0
  56. data/lib/active_support/core_ext/date_time/conversions.rb +107 -0
  57. data/lib/active_support/core_ext/digest.rb +3 -0
  58. data/lib/active_support/core_ext/digest/uuid.rb +53 -0
  59. data/lib/active_support/core_ext/enumerable.rb +188 -0
  60. data/lib/active_support/core_ext/file.rb +3 -0
  61. data/lib/active_support/core_ext/file/atomic.rb +70 -0
  62. data/lib/active_support/core_ext/hash.rb +10 -0
  63. data/lib/active_support/core_ext/hash/compact.rb +5 -0
  64. data/lib/active_support/core_ext/hash/conversions.rb +263 -0
  65. data/lib/active_support/core_ext/hash/deep_merge.rb +34 -0
  66. data/lib/active_support/core_ext/hash/deep_transform_values.rb +46 -0
  67. data/lib/active_support/core_ext/hash/except.rb +24 -0
  68. data/lib/active_support/core_ext/hash/indifferent_access.rb +24 -0
  69. data/lib/active_support/core_ext/hash/keys.rb +143 -0
  70. data/lib/active_support/core_ext/hash/reverse_merge.rb +25 -0
  71. data/lib/active_support/core_ext/hash/slice.rb +26 -0
  72. data/lib/active_support/core_ext/hash/transform_values.rb +5 -0
  73. data/lib/active_support/core_ext/integer.rb +5 -0
  74. data/lib/active_support/core_ext/integer/inflections.rb +31 -0
  75. data/lib/active_support/core_ext/integer/multiple.rb +12 -0
  76. data/lib/active_support/core_ext/integer/time.rb +22 -0
  77. data/lib/active_support/core_ext/kernel.rb +5 -0
  78. data/lib/active_support/core_ext/kernel/concern.rb +14 -0
  79. data/lib/active_support/core_ext/kernel/reporting.rb +45 -0
  80. data/lib/active_support/core_ext/kernel/singleton_class.rb +8 -0
  81. data/lib/active_support/core_ext/load_error.rb +9 -0
  82. data/lib/active_support/core_ext/marshal.rb +24 -0
  83. data/lib/active_support/core_ext/module.rb +13 -0
  84. data/lib/active_support/core_ext/module/aliasing.rb +31 -0
  85. data/lib/active_support/core_ext/module/anonymous.rb +30 -0
  86. data/lib/active_support/core_ext/module/attr_internal.rb +38 -0
  87. data/lib/active_support/core_ext/module/attribute_accessors.rb +212 -0
  88. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +144 -0
  89. data/lib/active_support/core_ext/module/concerning.rb +134 -0
  90. data/lib/active_support/core_ext/module/delegation.rb +313 -0
  91. data/lib/active_support/core_ext/module/deprecation.rb +25 -0
  92. data/lib/active_support/core_ext/module/introspection.rb +86 -0
  93. data/lib/active_support/core_ext/module/reachable.rb +6 -0
  94. data/lib/active_support/core_ext/module/redefine_method.rb +40 -0
  95. data/lib/active_support/core_ext/module/remove_method.rb +17 -0
  96. data/lib/active_support/core_ext/name_error.rb +38 -0
  97. data/lib/active_support/core_ext/numeric.rb +5 -0
  98. data/lib/active_support/core_ext/numeric/bytes.rb +66 -0
  99. data/lib/active_support/core_ext/numeric/conversions.rb +136 -0
  100. data/lib/active_support/core_ext/numeric/inquiry.rb +5 -0
  101. data/lib/active_support/core_ext/numeric/time.rb +66 -0
  102. data/lib/active_support/core_ext/object.rb +16 -0
  103. data/lib/active_support/core_ext/object/acts_like.rb +21 -0
  104. data/lib/active_support/core_ext/object/blank.rb +155 -0
  105. data/lib/active_support/core_ext/object/conversions.rb +6 -0
  106. data/lib/active_support/core_ext/object/deep_dup.rb +55 -0
  107. data/lib/active_support/core_ext/object/duplicable.rb +49 -0
  108. data/lib/active_support/core_ext/object/inclusion.rb +29 -0
  109. data/lib/active_support/core_ext/object/instance_variables.rb +30 -0
  110. data/lib/active_support/core_ext/object/json.rb +228 -0
  111. data/lib/active_support/core_ext/object/to_param.rb +3 -0
  112. data/lib/active_support/core_ext/object/to_query.rb +89 -0
  113. data/lib/active_support/core_ext/object/try.rb +156 -0
  114. data/lib/active_support/core_ext/object/with_options.rb +82 -0
  115. data/lib/active_support/core_ext/range.rb +7 -0
  116. data/lib/active_support/core_ext/range/compare_range.rb +70 -0
  117. data/lib/active_support/core_ext/range/conversions.rb +41 -0
  118. data/lib/active_support/core_ext/range/each.rb +25 -0
  119. data/lib/active_support/core_ext/range/include_range.rb +9 -0
  120. data/lib/active_support/core_ext/range/include_time_with_zone.rb +23 -0
  121. data/lib/active_support/core_ext/range/overlaps.rb +10 -0
  122. data/lib/active_support/core_ext/regexp.rb +7 -0
  123. data/lib/active_support/core_ext/securerandom.rb +45 -0
  124. data/lib/active_support/core_ext/string.rb +15 -0
  125. data/lib/active_support/core_ext/string/access.rb +114 -0
  126. data/lib/active_support/core_ext/string/behavior.rb +8 -0
  127. data/lib/active_support/core_ext/string/conversions.rb +59 -0
  128. data/lib/active_support/core_ext/string/exclude.rb +13 -0
  129. data/lib/active_support/core_ext/string/filters.rb +145 -0
  130. data/lib/active_support/core_ext/string/indent.rb +45 -0
  131. data/lib/active_support/core_ext/string/inflections.rb +259 -0
  132. data/lib/active_support/core_ext/string/inquiry.rb +15 -0
  133. data/lib/active_support/core_ext/string/multibyte.rb +58 -0
  134. data/lib/active_support/core_ext/string/output_safety.rb +314 -0
  135. data/lib/active_support/core_ext/string/starts_ends_with.rb +6 -0
  136. data/lib/active_support/core_ext/string/strip.rb +27 -0
  137. data/lib/active_support/core_ext/string/zones.rb +16 -0
  138. data/lib/active_support/core_ext/time.rb +7 -0
  139. data/lib/active_support/core_ext/time/acts_like.rb +10 -0
  140. data/lib/active_support/core_ext/time/calculations.rb +344 -0
  141. data/lib/active_support/core_ext/time/compatibility.rb +16 -0
  142. data/lib/active_support/core_ext/time/conversions.rb +72 -0
  143. data/lib/active_support/core_ext/time/zones.rb +113 -0
  144. data/lib/active_support/core_ext/uri.rb +25 -0
  145. data/lib/active_support/current_attributes.rb +203 -0
  146. data/lib/active_support/dependencies.rb +806 -0
  147. data/lib/active_support/dependencies/autoload.rb +79 -0
  148. data/lib/active_support/dependencies/interlock.rb +57 -0
  149. data/lib/active_support/dependencies/zeitwerk_integration.rb +110 -0
  150. data/lib/active_support/deprecation.rb +46 -0
  151. data/lib/active_support/deprecation/behaviors.rb +109 -0
  152. data/lib/active_support/deprecation/constant_accessor.rb +52 -0
  153. data/lib/active_support/deprecation/instance_delegator.rb +39 -0
  154. data/lib/active_support/deprecation/method_wrappers.rb +78 -0
  155. data/lib/active_support/deprecation/proxy_wrappers.rb +173 -0
  156. data/lib/active_support/deprecation/reporting.rb +114 -0
  157. data/lib/active_support/descendants_tracker.rb +109 -0
  158. data/lib/active_support/digest.rb +20 -0
  159. data/lib/active_support/duration.rb +433 -0
  160. data/lib/active_support/duration/iso8601_parser.rb +124 -0
  161. data/lib/active_support/duration/iso8601_serializer.rb +54 -0
  162. data/lib/active_support/encrypted_configuration.rb +45 -0
  163. data/lib/active_support/encrypted_file.rb +100 -0
  164. data/lib/active_support/evented_file_update_checker.rb +235 -0
  165. data/lib/active_support/execution_wrapper.rb +129 -0
  166. data/lib/active_support/executor.rb +8 -0
  167. data/lib/active_support/file_update_checker.rb +163 -0
  168. data/lib/active_support/gem_version.rb +17 -0
  169. data/lib/active_support/gzip.rb +38 -0
  170. data/lib/active_support/hash_with_indifferent_access.rb +399 -0
  171. data/lib/active_support/i18n.rb +16 -0
  172. data/lib/active_support/i18n_railtie.rb +126 -0
  173. data/lib/active_support/inflections.rb +72 -0
  174. data/lib/active_support/inflector.rb +9 -0
  175. data/lib/active_support/inflector/inflections.rb +257 -0
  176. data/lib/active_support/inflector/methods.rb +398 -0
  177. data/lib/active_support/inflector/transliterate.rb +147 -0
  178. data/lib/active_support/json.rb +4 -0
  179. data/lib/active_support/json/decoding.rb +76 -0
  180. data/lib/active_support/json/encoding.rb +134 -0
  181. data/lib/active_support/key_generator.rb +41 -0
  182. data/lib/active_support/lazy_load_hooks.rb +82 -0
  183. data/lib/active_support/locale/en.rb +31 -0
  184. data/lib/active_support/locale/en.yml +135 -0
  185. data/lib/active_support/log_subscriber.rb +135 -0
  186. data/lib/active_support/log_subscriber/test_helper.rb +106 -0
  187. data/lib/active_support/logger.rb +93 -0
  188. data/lib/active_support/logger_silence.rb +45 -0
  189. data/lib/active_support/logger_thread_safe_level.rb +56 -0
  190. data/lib/active_support/message_encryptor.rb +227 -0
  191. data/lib/active_support/message_verifier.rb +205 -0
  192. data/lib/active_support/messages/metadata.rb +71 -0
  193. data/lib/active_support/messages/rotation_configuration.rb +22 -0
  194. data/lib/active_support/messages/rotator.rb +56 -0
  195. data/lib/active_support/multibyte.rb +23 -0
  196. data/lib/active_support/multibyte/chars.rb +216 -0
  197. data/lib/active_support/multibyte/unicode.rb +157 -0
  198. data/lib/active_support/notifications.rb +253 -0
  199. data/lib/active_support/notifications/fanout.rb +244 -0
  200. data/lib/active_support/notifications/instrumenter.rb +164 -0
  201. data/lib/active_support/number_helper.rb +378 -0
  202. data/lib/active_support/number_helper/number_converter.rb +184 -0
  203. data/lib/active_support/number_helper/number_to_currency_converter.rb +46 -0
  204. data/lib/active_support/number_helper/number_to_delimited_converter.rb +31 -0
  205. data/lib/active_support/number_helper/number_to_human_converter.rb +70 -0
  206. data/lib/active_support/number_helper/number_to_human_size_converter.rb +61 -0
  207. data/lib/active_support/number_helper/number_to_percentage_converter.rb +16 -0
  208. data/lib/active_support/number_helper/number_to_phone_converter.rb +60 -0
  209. data/lib/active_support/number_helper/number_to_rounded_converter.rb +56 -0
  210. data/lib/active_support/number_helper/rounding_helper.rb +66 -0
  211. data/lib/active_support/option_merger.rb +27 -0
  212. data/lib/active_support/ordered_hash.rb +50 -0
  213. data/lib/active_support/ordered_options.rb +85 -0
  214. data/lib/active_support/parameter_filter.rb +129 -0
  215. data/lib/active_support/per_thread_registry.rb +60 -0
  216. data/lib/active_support/proxy_object.rb +15 -0
  217. data/lib/active_support/rails.rb +29 -0
  218. data/lib/active_support/railtie.rb +80 -0
  219. data/lib/active_support/reloader.rb +130 -0
  220. data/lib/active_support/rescuable.rb +174 -0
  221. data/lib/active_support/security_utils.rb +31 -0
  222. data/lib/active_support/string_inquirer.rb +34 -0
  223. data/lib/active_support/subscriber.rb +169 -0
  224. data/lib/active_support/tagged_logging.rb +88 -0
  225. data/lib/active_support/test_case.rb +163 -0
  226. data/lib/active_support/testing/assertions.rb +228 -0
  227. data/lib/active_support/testing/autorun.rb +7 -0
  228. data/lib/active_support/testing/constant_lookup.rb +51 -0
  229. data/lib/active_support/testing/declarative.rb +28 -0
  230. data/lib/active_support/testing/deprecation.rb +38 -0
  231. data/lib/active_support/testing/file_fixtures.rb +38 -0
  232. data/lib/active_support/testing/isolation.rb +110 -0
  233. data/lib/active_support/testing/method_call_assertions.rb +70 -0
  234. data/lib/active_support/testing/parallelization.rb +128 -0
  235. data/lib/active_support/testing/setup_and_teardown.rb +55 -0
  236. data/lib/active_support/testing/stream.rb +44 -0
  237. data/lib/active_support/testing/tagged_logging.rb +27 -0
  238. data/lib/active_support/testing/time_helpers.rb +200 -0
  239. data/lib/active_support/time.rb +20 -0
  240. data/lib/active_support/time_with_zone.rb +561 -0
  241. data/lib/active_support/values/time_zone.rb +570 -0
  242. data/lib/active_support/version.rb +10 -0
  243. data/lib/active_support/xml_mini.rb +202 -0
  244. data/lib/active_support/xml_mini/jdom.rb +183 -0
  245. data/lib/active_support/xml_mini/libxml.rb +80 -0
  246. data/lib/active_support/xml_mini/libxmlsax.rb +83 -0
  247. data/lib/active_support/xml_mini/nokogiri.rb +83 -0
  248. data/lib/active_support/xml_mini/nokogirisax.rb +86 -0
  249. data/lib/active_support/xml_mini/rexml.rb +130 -0
  250. metadata +385 -0
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+
5
+ module ActiveSupport
6
+ class Duration
7
+ # Parses a string formatted according to ISO 8601 Duration into the hash.
8
+ #
9
+ # See {ISO 8601}[https://en.wikipedia.org/wiki/ISO_8601#Durations] for more information.
10
+ #
11
+ # This parser allows negative parts to be present in pattern.
12
+ class ISO8601Parser # :nodoc:
13
+ class ParsingError < ::ArgumentError; end
14
+
15
+ PERIOD_OR_COMMA = /\.|,/
16
+ PERIOD = "."
17
+ COMMA = ","
18
+
19
+ SIGN_MARKER = /\A\-|\+|/
20
+ DATE_MARKER = /P/
21
+ TIME_MARKER = /T/
22
+ DATE_COMPONENT = /(\-?\d+(?:[.,]\d+)?)(Y|M|D|W)/
23
+ TIME_COMPONENT = /(\-?\d+(?:[.,]\d+)?)(H|M|S)/
24
+
25
+ DATE_TO_PART = { "Y" => :years, "M" => :months, "W" => :weeks, "D" => :days }
26
+ TIME_TO_PART = { "H" => :hours, "M" => :minutes, "S" => :seconds }
27
+
28
+ DATE_COMPONENTS = [:years, :months, :days]
29
+ TIME_COMPONENTS = [:hours, :minutes, :seconds]
30
+
31
+ attr_reader :parts, :scanner
32
+ attr_accessor :mode, :sign
33
+
34
+ def initialize(string)
35
+ @scanner = StringScanner.new(string)
36
+ @parts = {}
37
+ @mode = :start
38
+ @sign = 1
39
+ end
40
+
41
+ def parse!
42
+ while !finished?
43
+ case mode
44
+ when :start
45
+ if scan(SIGN_MARKER)
46
+ self.sign = (scanner.matched == "-") ? -1 : 1
47
+ self.mode = :sign
48
+ else
49
+ raise_parsing_error
50
+ end
51
+
52
+ when :sign
53
+ if scan(DATE_MARKER)
54
+ self.mode = :date
55
+ else
56
+ raise_parsing_error
57
+ end
58
+
59
+ when :date
60
+ if scan(TIME_MARKER)
61
+ self.mode = :time
62
+ elsif scan(DATE_COMPONENT)
63
+ parts[DATE_TO_PART[scanner[2]]] = number * sign
64
+ else
65
+ raise_parsing_error
66
+ end
67
+
68
+ when :time
69
+ if scan(TIME_COMPONENT)
70
+ parts[TIME_TO_PART[scanner[2]]] = number * sign
71
+ else
72
+ raise_parsing_error
73
+ end
74
+
75
+ end
76
+ end
77
+
78
+ validate!
79
+ parts
80
+ end
81
+
82
+ private
83
+
84
+ def finished?
85
+ scanner.eos?
86
+ end
87
+
88
+ # Parses number which can be a float with either comma or period.
89
+ def number
90
+ PERIOD_OR_COMMA.match?(scanner[1]) ? scanner[1].tr(COMMA, PERIOD).to_f : scanner[1].to_i
91
+ end
92
+
93
+ def scan(pattern)
94
+ scanner.scan(pattern)
95
+ end
96
+
97
+ def raise_parsing_error(reason = nil)
98
+ raise ParsingError, "Invalid ISO 8601 duration: #{scanner.string.inspect} #{reason}".strip
99
+ end
100
+
101
+ # Checks for various semantic errors as stated in ISO 8601 standard.
102
+ def validate!
103
+ raise_parsing_error("is empty duration") if parts.empty?
104
+
105
+ # Mixing any of Y, M, D with W is invalid.
106
+ if parts.key?(:weeks) && (parts.keys & DATE_COMPONENTS).any?
107
+ raise_parsing_error("mixing weeks with other date parts not allowed")
108
+ end
109
+
110
+ # Specifying an empty T part is invalid.
111
+ if mode == :time && (parts.keys & TIME_COMPONENTS).empty?
112
+ raise_parsing_error("time part marker is present but time part is empty")
113
+ end
114
+
115
+ fractions = parts.values.reject(&:zero?).select { |a| (a % 1) != 0 }
116
+ unless fractions.empty? || (fractions.size == 1 && fractions.last == @parts.values.reject(&:zero?).last)
117
+ raise_parsing_error "(only last part can be fractional)"
118
+ end
119
+
120
+ true
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/object/blank"
4
+
5
+ module ActiveSupport
6
+ class Duration
7
+ # Serializes duration to string according to ISO 8601 Duration format.
8
+ class ISO8601Serializer # :nodoc:
9
+ def initialize(duration, precision: nil)
10
+ @duration = duration
11
+ @precision = precision
12
+ end
13
+
14
+ # Builds and returns output string.
15
+ def serialize
16
+ parts, sign = normalize
17
+ return "PT0S" if parts.empty?
18
+
19
+ output = +"P"
20
+ output << "#{parts[:years]}Y" if parts.key?(:years)
21
+ output << "#{parts[:months]}M" if parts.key?(:months)
22
+ output << "#{parts[:weeks]}W" if parts.key?(:weeks)
23
+ output << "#{parts[:days]}D" if parts.key?(:days)
24
+ time = +""
25
+ time << "#{parts[:hours]}H" if parts.key?(:hours)
26
+ time << "#{parts[:minutes]}M" if parts.key?(:minutes)
27
+ if parts.key?(:seconds)
28
+ time << "#{sprintf(@precision ? "%0.0#{@precision}f" : '%g', parts[:seconds])}S"
29
+ end
30
+ output << "T#{time}" unless time.empty?
31
+ "#{sign}#{output}"
32
+ end
33
+
34
+ private
35
+
36
+ # Return pair of duration's parts and whole duration sign.
37
+ # Parts are summarized (as they can become repetitive due to addition, etc).
38
+ # Zero parts are removed as not significant.
39
+ # If all parts are negative it will negate all of them and return minus as a sign.
40
+ def normalize
41
+ parts = @duration.parts.each_with_object(Hash.new(0)) do |(k, v), p|
42
+ p[k] += v unless v.zero?
43
+ end
44
+ # If all parts are negative - let's make a negative duration
45
+ sign = ""
46
+ if parts.values.all? { |v| v < 0 }
47
+ sign = "-"
48
+ parts.transform_values!(&:-@)
49
+ end
50
+ [parts, sign]
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "active_support/encrypted_file"
5
+ require "active_support/ordered_options"
6
+ require "active_support/core_ext/object/inclusion"
7
+ require "active_support/core_ext/module/delegation"
8
+
9
+ module ActiveSupport
10
+ class EncryptedConfiguration < EncryptedFile
11
+ delegate :[], :fetch, to: :config
12
+ delegate_missing_to :options
13
+
14
+ def initialize(config_path:, key_path:, env_key:, raise_if_missing_key:)
15
+ super content_path: config_path, key_path: key_path,
16
+ env_key: env_key, raise_if_missing_key: raise_if_missing_key
17
+ end
18
+
19
+ # Allow a config to be started without a file present
20
+ def read
21
+ super
22
+ rescue ActiveSupport::EncryptedFile::MissingContentError
23
+ ""
24
+ end
25
+
26
+ def write(contents)
27
+ deserialize(contents)
28
+
29
+ super
30
+ end
31
+
32
+ def config
33
+ @config ||= deserialize(read).deep_symbolize_keys
34
+ end
35
+
36
+ private
37
+ def options
38
+ @options ||= ActiveSupport::InheritableOptions.new(config)
39
+ end
40
+
41
+ def deserialize(config)
42
+ YAML.load(config).presence || {}
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "tmpdir"
5
+ require "active_support/message_encryptor"
6
+
7
+ module ActiveSupport
8
+ class EncryptedFile
9
+ class MissingContentError < RuntimeError
10
+ def initialize(content_path)
11
+ super "Missing encrypted content file in #{content_path}."
12
+ end
13
+ end
14
+
15
+ class MissingKeyError < RuntimeError
16
+ def initialize(key_path:, env_key:)
17
+ super \
18
+ "Missing encryption key to decrypt file with. " +
19
+ "Ask your team for your master key and write it to #{key_path} or put it in the ENV['#{env_key}']."
20
+ end
21
+ end
22
+
23
+ CIPHER = "aes-128-gcm"
24
+
25
+ def self.generate_key
26
+ SecureRandom.hex(ActiveSupport::MessageEncryptor.key_len(CIPHER))
27
+ end
28
+
29
+
30
+ attr_reader :content_path, :key_path, :env_key, :raise_if_missing_key
31
+
32
+ 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)
34
+ @env_key, @raise_if_missing_key = env_key, raise_if_missing_key
35
+ end
36
+
37
+ def key
38
+ read_env_key || read_key_file || handle_missing_key
39
+ end
40
+
41
+ def read
42
+ if !key.nil? && content_path.exist?
43
+ decrypt content_path.binread
44
+ else
45
+ raise MissingContentError, content_path
46
+ end
47
+ end
48
+
49
+ def write(contents)
50
+ IO.binwrite "#{content_path}.tmp", encrypt(contents)
51
+ FileUtils.mv "#{content_path}.tmp", content_path
52
+ end
53
+
54
+ def change(&block)
55
+ writing read, &block
56
+ end
57
+
58
+
59
+ private
60
+ 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
64
+
65
+ yield tmp_path
66
+
67
+ updated_contents = tmp_path.binread
68
+
69
+ write(updated_contents) if updated_contents != contents
70
+ ensure
71
+ FileUtils.rm(tmp_path) if tmp_path&.exist?
72
+ end
73
+
74
+
75
+ def encrypt(contents)
76
+ encryptor.encrypt_and_sign contents
77
+ end
78
+
79
+ def decrypt(contents)
80
+ encryptor.decrypt_and_verify contents
81
+ end
82
+
83
+ def encryptor
84
+ @encryptor ||= ActiveSupport::MessageEncryptor.new([ key ].pack("H*"), cipher: CIPHER)
85
+ end
86
+
87
+
88
+ def read_env_key
89
+ ENV[env_key]
90
+ end
91
+
92
+ def read_key_file
93
+ key_path.binread.strip if key_path.exist?
94
+ end
95
+
96
+ def handle_missing_key
97
+ raise MissingKeyError, key_path: key_path, env_key: env_key if raise_if_missing_key
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+ require "pathname"
5
+ require "concurrent/atomic/atomic_boolean"
6
+
7
+ module ActiveSupport
8
+ # Allows you to "listen" to changes in a file system.
9
+ # The evented file updater does not hit disk when checking for updates
10
+ # instead it uses platform specific file system events to trigger a change
11
+ # in state.
12
+ #
13
+ # The file checker takes an array of files to watch or a hash specifying directories
14
+ # and file extensions to watch. It also takes a block that is called when
15
+ # EventedFileUpdateChecker#execute is run or when EventedFileUpdateChecker#execute_if_updated
16
+ # is run and there have been changes to the file system.
17
+ #
18
+ # Note: Forking will cause the first call to `updated?` to return `true`.
19
+ #
20
+ # Example:
21
+ #
22
+ # checker = ActiveSupport::EventedFileUpdateChecker.new(["/tmp/foo"]) { puts "changed" }
23
+ # checker.updated?
24
+ # # => false
25
+ # checker.execute_if_updated
26
+ # # => nil
27
+ #
28
+ # FileUtils.touch("/tmp/foo")
29
+ #
30
+ # checker.updated?
31
+ # # => true
32
+ # checker.execute_if_updated
33
+ # # => "changed"
34
+ #
35
+ class EventedFileUpdateChecker #:nodoc: all
36
+ def initialize(files, dirs = {}, &block)
37
+ unless block
38
+ raise ArgumentError, "A block is required to initialize an EventedFileUpdateChecker"
39
+ end
40
+
41
+ @ph = PathHelper.new
42
+ @files = files.map { |f| @ph.xpath(f) }.to_set
43
+
44
+ @dirs = {}
45
+ dirs.each do |dir, exts|
46
+ @dirs[@ph.xpath(dir)] = Array(exts).map { |ext| @ph.normalize_extension(ext) }
47
+ end
48
+
49
+ @block = block
50
+ @updated = Concurrent::AtomicBoolean.new(false)
51
+ @lcsp = @ph.longest_common_subpath(@dirs.keys)
52
+ @pid = Process.pid
53
+ @boot_mutex = Mutex.new
54
+
55
+ dtw = directories_to_watch
56
+ @dtw, @missing = dtw.partition(&:exist?)
57
+
58
+ if @dtw.any?
59
+ # Loading listen triggers warnings. These are originated by a legit
60
+ # usage of attr_* macros for private attributes, but adds a lot of noise
61
+ # to our test suite. Thus, we lazy load it and disable warnings locally.
62
+ silence_warnings do
63
+ require "listen"
64
+ rescue LoadError => e
65
+ raise LoadError, "Could not load the 'listen' gem. Add `gem 'listen'` to the development group of your Gemfile", e.backtrace
66
+ end
67
+ end
68
+ boot!
69
+ end
70
+
71
+ def updated?
72
+ @boot_mutex.synchronize do
73
+ if @pid != Process.pid
74
+ boot!
75
+ @pid = Process.pid
76
+ @updated.make_true
77
+ end
78
+ end
79
+
80
+ if @missing.any?(&:exist?)
81
+ @boot_mutex.synchronize do
82
+ appeared, @missing = @missing.partition(&:exist?)
83
+ shutdown!
84
+
85
+ @dtw += appeared
86
+ boot!
87
+
88
+ @updated.make_true
89
+ end
90
+ end
91
+
92
+ @updated.true?
93
+ end
94
+
95
+ def execute
96
+ @updated.make_false
97
+ @block.call
98
+ end
99
+
100
+ def execute_if_updated
101
+ if updated?
102
+ yield if block_given?
103
+ execute
104
+ true
105
+ end
106
+ end
107
+
108
+ private
109
+ def boot!
110
+ normalize_dirs!
111
+
112
+ unless @dtw.empty?
113
+ Listen.to(*@dtw, &method(:changed)).start
114
+ end
115
+ end
116
+
117
+ def shutdown!
118
+ Listen.stop
119
+ end
120
+
121
+ def normalize_dirs!
122
+ @dirs.transform_keys! do |dir|
123
+ dir.exist? ? dir.realpath : dir
124
+ end
125
+ end
126
+
127
+ def changed(modified, added, removed)
128
+ unless updated?
129
+ @updated.make_true if (modified + added + removed).any? { |f| watching?(f) }
130
+ end
131
+ end
132
+
133
+ def watching?(file)
134
+ file = @ph.xpath(file)
135
+
136
+ if @files.member?(file)
137
+ true
138
+ elsif file.directory?
139
+ false
140
+ else
141
+ ext = @ph.normalize_extension(file.extname)
142
+
143
+ file.dirname.ascend do |dir|
144
+ matching = @dirs[dir]
145
+
146
+ if matching && (matching.empty? || matching.include?(ext))
147
+ break true
148
+ elsif dir == @lcsp || dir.root?
149
+ break false
150
+ end
151
+ end
152
+ end
153
+ end
154
+
155
+ def directories_to_watch
156
+ dtw = @files.map(&:dirname) + @dirs.keys
157
+ dtw.compact!
158
+ dtw.uniq!
159
+
160
+ normalized_gem_paths = Gem.path.map { |path| File.join path, "" }
161
+ dtw = dtw.reject do |path|
162
+ normalized_gem_paths.any? { |gem_path| path.to_s.start_with?(gem_path) }
163
+ end
164
+
165
+ @ph.filter_out_descendants(dtw)
166
+ end
167
+
168
+ class PathHelper
169
+ def xpath(path)
170
+ Pathname.new(path).expand_path
171
+ end
172
+
173
+ def normalize_extension(ext)
174
+ ext.to_s.sub(/\A\./, "")
175
+ end
176
+
177
+ # Given a collection of Pathname objects returns the longest subpath
178
+ # common to all of them, or +nil+ if there is none.
179
+ def longest_common_subpath(paths)
180
+ return if paths.empty?
181
+
182
+ lcsp = Pathname.new(paths[0])
183
+
184
+ paths[1..-1].each do |path|
185
+ until ascendant_of?(lcsp, path)
186
+ if lcsp.root?
187
+ # If we get here a root directory is not an ascendant of path.
188
+ # This may happen if there are paths in different drives on
189
+ # Windows.
190
+ return
191
+ else
192
+ lcsp = lcsp.parent
193
+ end
194
+ end
195
+ end
196
+
197
+ lcsp
198
+ end
199
+
200
+ # Returns the deepest existing ascendant, which could be the argument itself.
201
+ def existing_parent(dir)
202
+ dir.ascend do |ascendant|
203
+ break ascendant if ascendant.directory?
204
+ end
205
+ end
206
+
207
+ # Filters out directories which are descendants of others in the collection (stable).
208
+ def filter_out_descendants(dirs)
209
+ return dirs if dirs.length < 2
210
+
211
+ dirs_sorted_by_nparts = dirs.sort_by { |dir| dir.each_filename.to_a.length }
212
+ descendants = []
213
+
214
+ until dirs_sorted_by_nparts.empty?
215
+ dir = dirs_sorted_by_nparts.shift
216
+
217
+ dirs_sorted_by_nparts.reject! do |possible_descendant|
218
+ ascendant_of?(dir, possible_descendant) && descendants << possible_descendant
219
+ end
220
+ end
221
+
222
+ # Array#- preserves order.
223
+ dirs - descendants
224
+ end
225
+
226
+ private
227
+
228
+ def ascendant_of?(base, other)
229
+ base != other && other.ascend do |ascendant|
230
+ break true if base == ascendant
231
+ end
232
+ end
233
+ end
234
+ end
235
+ end