runger_config 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +562 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +1121 -0
  5. data/lib/anyway/auto_cast.rb +53 -0
  6. data/lib/anyway/config.rb +473 -0
  7. data/lib/anyway/dynamic_config.rb +31 -0
  8. data/lib/anyway/ejson_parser.rb +40 -0
  9. data/lib/anyway/env.rb +73 -0
  10. data/lib/anyway/ext/deep_dup.rb +48 -0
  11. data/lib/anyway/ext/deep_freeze.rb +44 -0
  12. data/lib/anyway/ext/flatten_names.rb +37 -0
  13. data/lib/anyway/ext/hash.rb +40 -0
  14. data/lib/anyway/ext/string_constantize.rb +24 -0
  15. data/lib/anyway/loaders/base.rb +21 -0
  16. data/lib/anyway/loaders/doppler.rb +63 -0
  17. data/lib/anyway/loaders/ejson.rb +89 -0
  18. data/lib/anyway/loaders/env.rb +18 -0
  19. data/lib/anyway/loaders/yaml.rb +84 -0
  20. data/lib/anyway/loaders.rb +79 -0
  21. data/lib/anyway/option_parser_builder.rb +29 -0
  22. data/lib/anyway/optparse_config.rb +92 -0
  23. data/lib/anyway/rails/autoload.rb +42 -0
  24. data/lib/anyway/rails/config.rb +23 -0
  25. data/lib/anyway/rails/loaders/credentials.rb +64 -0
  26. data/lib/anyway/rails/loaders/secrets.rb +37 -0
  27. data/lib/anyway/rails/loaders/yaml.rb +9 -0
  28. data/lib/anyway/rails/loaders.rb +5 -0
  29. data/lib/anyway/rails/settings.rb +83 -0
  30. data/lib/anyway/rails.rb +24 -0
  31. data/lib/anyway/railtie.rb +28 -0
  32. data/lib/anyway/rbs.rb +92 -0
  33. data/lib/anyway/settings.rb +111 -0
  34. data/lib/anyway/testing/helpers.rb +36 -0
  35. data/lib/anyway/testing.rb +13 -0
  36. data/lib/anyway/tracing.rb +188 -0
  37. data/lib/anyway/type_casting.rb +144 -0
  38. data/lib/anyway/utils/deep_merge.rb +21 -0
  39. data/lib/anyway/utils/which.rb +18 -0
  40. data/lib/anyway/version.rb +5 -0
  41. data/lib/anyway.rb +3 -0
  42. data/lib/anyway_config.rb +54 -0
  43. data/lib/generators/anyway/app_config/USAGE +9 -0
  44. data/lib/generators/anyway/app_config/app_config_generator.rb +17 -0
  45. data/lib/generators/anyway/config/USAGE +13 -0
  46. data/lib/generators/anyway/config/config_generator.rb +51 -0
  47. data/lib/generators/anyway/config/templates/config.rb.tt +12 -0
  48. data/lib/generators/anyway/config/templates/config.yml.tt +13 -0
  49. data/lib/generators/anyway/install/USAGE +4 -0
  50. data/lib/generators/anyway/install/install_generator.rb +47 -0
  51. data/lib/generators/anyway/install/templates/application_config.rb.tt +17 -0
  52. data/sig/anyway_config.rbs +149 -0
  53. data/sig/manifest.yml +6 -0
  54. metadata +202 -0
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anyway
4
+ module AutoCast
5
+ # Regexp to detect array values
6
+ # Array value is a values that contains at least one comma
7
+ # and doesn't start/end with quote or curly braces
8
+ ARRAY_RXP = /\A[^'"{].*\s*,\s*.*[^'"}]\z/
9
+
10
+ class << self
11
+ def call(val)
12
+ return val unless val.is_a?(::Hash) || val.is_a?(::String)
13
+
14
+ case val
15
+ when Hash
16
+ val.transform_values { call(_1) }
17
+ when ARRAY_RXP
18
+ val.split(/\s*,\s*/).map { call(_1) }
19
+ when /\A(true|t|yes|y)\z/i
20
+ true
21
+ when /\A(false|f|no|n)\z/i
22
+ false
23
+ when /\A(nil|null)\z/i
24
+ nil
25
+ when /\A\d+\z/
26
+ val.to_i
27
+ when /\A\d*\.\d+\z/
28
+ val.to_f
29
+ when /\A['"].*['"]\z/
30
+ val.gsub(/(\A['"]|['"]\z)/, "")
31
+ else
32
+ val
33
+ end
34
+ end
35
+
36
+ def cast_hash(obj)
37
+ obj.transform_values do |val|
38
+ val.is_a?(::Hash) ? cast_hash(val) : call(val)
39
+ end
40
+ end
41
+
42
+ def coerce(_key, val)
43
+ call(val)
44
+ end
45
+ end
46
+ end
47
+
48
+ module NoCast
49
+ def self.call(val) = val
50
+
51
+ def self.coerce(_key, val) = val
52
+ end
53
+ end
@@ -0,0 +1,473 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "anyway/optparse_config"
4
+ require "anyway/dynamic_config"
5
+
6
+ module Anyway # :nodoc:
7
+ using RubyNext
8
+ using Anyway::Ext::DeepDup
9
+ using Anyway::Ext::DeepFreeze
10
+ using Anyway::Ext::Hash
11
+ using Anyway::Ext::FlattenNames
12
+
13
+ using(Module.new do
14
+ refine Object do
15
+ def vm_object_id() = (object_id << 1).to_s(16)
16
+ end
17
+ end)
18
+
19
+ # Base config class
20
+ # Provides `attr_config` method to describe
21
+ # configuration parameters and set defaults
22
+ class Config
23
+ PARAM_NAME = /^[a-z_](\w+)?$/
24
+
25
+ # List of names that couldn't be used as config names
26
+ # (the class instance methods we use)
27
+ RESERVED_NAMES = %i[
28
+ config_name
29
+ env_prefix
30
+ as_env
31
+ values
32
+ class
33
+ clear
34
+ deconstruct_keys
35
+ dig
36
+ dup
37
+ initialize
38
+ load
39
+ load_from_sources
40
+ option_parser
41
+ pretty_print
42
+ raise_validation_error
43
+ reload
44
+ resolve_config_path
45
+ tap
46
+ to_h
47
+ to_source_trace
48
+ write_config_attr
49
+ __type_caster__
50
+ ].freeze
51
+
52
+ class Error < StandardError; end
53
+
54
+ class ValidationError < Error; end
55
+
56
+ include OptparseConfig
57
+ include DynamicConfig
58
+
59
+ class BlockCallback
60
+ attr_reader :block
61
+
62
+ def initialize(block)
63
+ @block = block
64
+ end
65
+
66
+ def apply_to(config)
67
+ config.instance_exec(&block)
68
+ end
69
+ end
70
+
71
+ class NamedCallback
72
+ attr_reader :name
73
+
74
+ def initialize(name)
75
+ @name = name
76
+ end
77
+
78
+ def apply_to(config) = config.send(name)
79
+ end
80
+
81
+ class << self
82
+ def attr_config(*args, **hargs)
83
+ new_defaults = hargs.deep_dup
84
+ new_defaults.stringify_keys!
85
+
86
+ defaults.merge! new_defaults
87
+
88
+ new_keys = ((args + new_defaults.keys) - config_attributes)
89
+
90
+ validate_param_names! new_keys.map(&:to_s)
91
+
92
+ new_keys.map!(&:to_sym)
93
+
94
+ unless (reserved_names = (new_keys & RESERVED_NAMES)).empty?
95
+ raise ArgumentError, "Can not use the following reserved names as config attrubutes: " \
96
+ "#{reserved_names.sort.map(&:to_s).join(", ")}"
97
+ end
98
+
99
+ config_attributes.push(*new_keys)
100
+
101
+ define_config_accessor(*new_keys)
102
+
103
+ # Define predicate methods ("param?") for attributes
104
+ # having `true` or `false` as default values
105
+ new_defaults.each do |key, val|
106
+ next unless val.is_a?(TrueClass) || val.is_a?(FalseClass)
107
+ alias_method :"#{key}?", :"#{key}"
108
+ end
109
+ end
110
+
111
+ def defaults
112
+ return @defaults if instance_variable_defined?(:@defaults)
113
+
114
+ @defaults = if superclass < Anyway::Config
115
+ superclass.defaults.deep_dup
116
+ else
117
+ new_empty_config
118
+ end
119
+ end
120
+
121
+ def config_attributes
122
+ return @config_attributes if instance_variable_defined?(:@config_attributes)
123
+
124
+ @config_attributes = if superclass < Anyway::Config
125
+ superclass.config_attributes.dup
126
+ else
127
+ []
128
+ end
129
+ end
130
+
131
+ def required(*names, env: nil, **nested)
132
+ unknown_names = names + nested.keys - config_attributes
133
+ raise ArgumentError, "Unknown config param: #{unknown_names.join(",")}" if unknown_names.any?
134
+
135
+ return unless Settings.matching_env?(env)
136
+
137
+ required_attributes.push(*names)
138
+ required_attributes.push(*nested.flatten_names)
139
+ end
140
+
141
+ def required_attributes
142
+ return @required_attributes if instance_variable_defined?(:@required_attributes)
143
+
144
+ @required_attributes = if superclass < Anyway::Config
145
+ superclass.required_attributes.dup
146
+ else
147
+ []
148
+ end
149
+ end
150
+
151
+ def on_load(*names, &block)
152
+ raise ArgumentError, "Either methods or block should be specified, not both" if block && !names.empty?
153
+
154
+ if block
155
+ load_callbacks << BlockCallback.new(block)
156
+ else
157
+ load_callbacks.push(*names.map { NamedCallback.new(_1) })
158
+ end
159
+ end
160
+
161
+ def load_callbacks
162
+ return @load_callbacks if instance_variable_defined?(:@load_callbacks)
163
+
164
+ @load_callbacks = if superclass <= Anyway::Config
165
+ superclass.load_callbacks.dup
166
+ else
167
+ []
168
+ end
169
+ end
170
+
171
+ def config_name(val = nil)
172
+ return (@explicit_config_name = val.to_s) unless val.nil?
173
+
174
+ return @config_name if instance_variable_defined?(:@config_name)
175
+
176
+ @config_name = explicit_config_name || build_config_name
177
+ end
178
+
179
+ def explicit_config_name
180
+ return @explicit_config_name if instance_variable_defined?(:@explicit_config_name)
181
+
182
+ @explicit_config_name =
183
+ if superclass.respond_to?(:explicit_config_name)
184
+ superclass.explicit_config_name
185
+ end
186
+ end
187
+
188
+ def explicit_config_name?() = !explicit_config_name.nil?
189
+
190
+ def env_prefix(val = nil)
191
+ return (@env_prefix = val.to_s.upcase) unless val.nil?
192
+
193
+ return @env_prefix if instance_variable_defined?(:@env_prefix)
194
+
195
+ @env_prefix = if superclass < Anyway::Config && superclass.explicit_config_name?
196
+ superclass.env_prefix
197
+ else
198
+ config_name.upcase
199
+ end
200
+ end
201
+
202
+ def loader_options(val = nil)
203
+ return (@loader_options = val) unless val.nil?
204
+
205
+ return @loader_options if instance_variable_defined?(:@loader_options)
206
+
207
+ @loader_options = if superclass < Anyway::Config
208
+ superclass.loader_options
209
+ else
210
+ {}
211
+ end
212
+ end
213
+
214
+ def new_empty_config() = {}
215
+
216
+ def coerce_types(mapping)
217
+ Utils.deep_merge!(coercion_mapping, mapping)
218
+
219
+ mapping.each do |key, val|
220
+ type = val.is_a?(::Hash) ? val[:type] : val
221
+ next if type != :boolean
222
+
223
+ alias_method :"#{key}?", :"#{key}"
224
+ end
225
+ end
226
+
227
+ def coercion_mapping
228
+ return @coercion_mapping if instance_variable_defined?(:@coercion_mapping)
229
+
230
+ @coercion_mapping = if superclass < Anyway::Config
231
+ superclass.coercion_mapping.deep_dup
232
+ else
233
+ {}
234
+ end
235
+ end
236
+
237
+ def type_caster(val = nil)
238
+ return @type_caster unless val.nil?
239
+
240
+ @type_caster ||=
241
+ if coercion_mapping.empty?
242
+ fallback_type_caster
243
+ else
244
+ ::Anyway::TypeCaster.new(coercion_mapping, fallback: fallback_type_caster)
245
+ end
246
+ end
247
+
248
+ def fallback_type_caster(val = nil)
249
+ return (@fallback_type_caster = val) unless val.nil?
250
+
251
+ return @fallback_type_caster if instance_variable_defined?(:@fallback_type_caster)
252
+
253
+ @fallback_type_caster = if superclass < Anyway::Config
254
+ superclass.fallback_type_caster.deep_dup
255
+ else
256
+ ::Anyway::AutoCast
257
+ end
258
+ end
259
+
260
+ def disable_auto_cast!
261
+ @fallback_type_caster = ::Anyway::NoCast
262
+ end
263
+
264
+ private
265
+
266
+ def define_config_accessor(*names)
267
+ names.each do |name|
268
+ accessors_module.module_eval <<~RUBY, __FILE__, __LINE__ + 1
269
+ def #{name}=(val)
270
+ __trace__&.record_value(val, "#{name}", **Tracing.current_trace_source)
271
+ values[:#{name}] = val
272
+ end
273
+
274
+ def #{name}
275
+ values[:#{name}]
276
+ end
277
+ RUBY
278
+ end
279
+ end
280
+
281
+ def accessors_module
282
+ return @accessors_module if instance_variable_defined?(:@accessors_module)
283
+
284
+ @accessors_module = Module.new.tap do |mod|
285
+ include mod
286
+ end
287
+ end
288
+
289
+ def build_config_name
290
+ unless name
291
+ raise "Please, specify config name explicitly for anonymous class " \
292
+ "via `config_name :my_config`"
293
+ end
294
+
295
+ # handle two cases:
296
+ # - SomeModule::Config => "some_module"
297
+ # - SomeConfig => "some"
298
+ unless name =~ /^(\w+)(::)?Config$/
299
+ raise "Couldn't infer config name, please, specify it explicitly " \
300
+ "via `config_name :my_config`"
301
+ end
302
+
303
+ # TODO(v3.0): Replace downcase with underscore
304
+ Regexp.last_match[1].tap(&:downcase!)
305
+ end
306
+
307
+ def validate_param_names!(names)
308
+ invalid_names = names.reject { |name| name =~ PARAM_NAME }
309
+ return if invalid_names.empty?
310
+
311
+ raise ArgumentError, "Invalid attr_config name: #{invalid_names.join(", ")}.\n" \
312
+ "Valid names must satisfy /#{PARAM_NAME.source}/."
313
+ end
314
+ end
315
+
316
+ on_load :validate_required_attributes!
317
+
318
+ attr_reader :config_name, :env_prefix
319
+
320
+ # Instantiate config instance.
321
+ #
322
+ # Example:
323
+ #
324
+ # my_config = Anyway::Config.new()
325
+ #
326
+ # # provide some values explicitly
327
+ # my_config = Anyway::Config.new({some: :value})
328
+ #
329
+ def initialize(overrides = nil)
330
+ @config_name = self.class.config_name
331
+
332
+ raise ArgumentError, "Config name is missing" unless @config_name
333
+
334
+ @env_prefix = self.class.env_prefix
335
+ @values = {}
336
+
337
+ load(overrides)
338
+ end
339
+
340
+ def reload(overrides = nil)
341
+ clear
342
+ load(overrides)
343
+ self
344
+ end
345
+
346
+ def clear
347
+ values.clear
348
+ @__trace__ = nil
349
+ self
350
+ end
351
+
352
+ def load(overrides = nil)
353
+ base_config = self.class.defaults.deep_dup
354
+
355
+ trace = Tracing.capture do
356
+ Tracing.trace!(:defaults) { base_config }
357
+
358
+ config_path = resolve_config_path(config_name, env_prefix)
359
+
360
+ load_from_sources(
361
+ base_config,
362
+ name: config_name,
363
+ env_prefix:,
364
+ config_path:,
365
+ **self.class.loader_options
366
+ )
367
+
368
+ if overrides
369
+ Tracing.trace!(:load) { overrides }
370
+
371
+ Utils.deep_merge!(base_config, overrides)
372
+ end
373
+ end
374
+
375
+ base_config.each do |key, val|
376
+ write_config_attr(key.to_sym, val)
377
+ end
378
+
379
+ # Trace may contain unknown attributes
380
+ trace&.keep_if { |key| self.class.config_attributes.include?(key.to_sym) }
381
+
382
+ # Run on_load callbacks
383
+ self.class.load_callbacks.each { _1.apply_to(self) }
384
+
385
+ # Set trace after we write all the values to
386
+ # avoid changing the source to accessor
387
+ @__trace__ = trace
388
+
389
+ self
390
+ end
391
+
392
+ def load_from_sources(base_config, **options)
393
+ Anyway.loaders.each do |(_id, loader)|
394
+ Utils.deep_merge!(base_config, loader.call(**options))
395
+ end
396
+ base_config
397
+ end
398
+
399
+ def dig(*keys) = values.dig(*keys)
400
+
401
+ def to_h() = values.deep_dup.deep_freeze
402
+
403
+ def dup
404
+ self.class.allocate.tap do |new_config|
405
+ %i[config_name env_prefix __trace__].each do |ivar|
406
+ new_config.instance_variable_set(:"@#{ivar}", send(ivar).dup)
407
+ end
408
+ new_config.instance_variable_set(:@values, values.deep_dup)
409
+ end
410
+ end
411
+
412
+ def resolve_config_path(name, env_prefix)
413
+ Anyway.env.fetch(env_prefix).delete("conf") || Settings.default_config_path.call(name)
414
+ end
415
+
416
+ def deconstruct_keys(keys) = values.deconstruct_keys(keys)
417
+
418
+ def to_source_trace() = __trace__&.to_h
419
+
420
+ def inspect
421
+ "#<#{self.class}:0x#{vm_object_id.rjust(16, "0")} config_name=\"#{config_name}\" env_prefix=\"#{env_prefix}\" " \
422
+ "values=#{values.inspect}>"
423
+ end
424
+
425
+ def pretty_print(q)
426
+ q.object_group self do
427
+ q.nest(1) do
428
+ q.breakable
429
+ q.text "config_name=#{config_name.inspect}"
430
+ q.breakable
431
+ q.text "env_prefix=#{env_prefix.inspect}"
432
+ q.breakable
433
+ q.text "values:"
434
+ q.pp __trace__
435
+ end
436
+ end
437
+ end
438
+
439
+ def as_env
440
+ Env.from_hash(to_h, prefix: env_prefix)
441
+ end
442
+
443
+ private
444
+
445
+ attr_reader :values, :__trace__
446
+
447
+ def validate_required_attributes!
448
+ self.class.required_attributes.select do |name|
449
+ val = values.dig(*name.to_s.split(".").map(&:to_sym))
450
+ val.nil? || (val.is_a?(String) && val.empty?)
451
+ end.then do |missing|
452
+ next if missing.empty?
453
+ raise_validation_error "The following config parameters for `#{self.class.name}(config_name: #{self.class.config_name})` are missing or empty: #{missing.join(", ")}"
454
+ end
455
+ end
456
+
457
+ def write_config_attr(key, val)
458
+ key = key.to_sym
459
+ return unless self.class.config_attributes.include?(key)
460
+
461
+ val = __type_caster__.coerce(key, val)
462
+ public_send(:"#{key}=", val)
463
+ end
464
+
465
+ def raise_validation_error(msg)
466
+ raise ValidationError, msg
467
+ end
468
+
469
+ def __type_caster__
470
+ self.class.type_caster
471
+ end
472
+ end
473
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anyway
4
+ # Adds ability to generate anonymous (class-less) config dynamicly
5
+ # (like Rails.application.config_for but using more data sources).
6
+ module DynamicConfig
7
+ module ClassMethods
8
+ # Load config as Hash by any name
9
+ #
10
+ # Example:
11
+ #
12
+ # my_config = Anyway::Config.for(:my_app)
13
+ # # will load data from config/my_app.yml, secrets.my_app, ENV["MY_APP_*"]
14
+ #
15
+ def for(name, auto_cast: true, **options)
16
+ config = allocate
17
+ options[:env_prefix] ||= name.to_s.upcase
18
+ options[:config_path] ||= config.resolve_config_path(name, options[:env_prefix])
19
+
20
+ raw_config = config.load_from_sources(new_empty_config, name:, **options)
21
+ return raw_config unless auto_cast
22
+
23
+ AutoCast.call(raw_config)
24
+ end
25
+ end
26
+
27
+ def self.included(base)
28
+ base.extend ClassMethods
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "anyway/ext/hash"
5
+
6
+ using Anyway::Ext::Hash
7
+
8
+ module Anyway
9
+ class EJSONParser
10
+ attr_reader :bin_path
11
+
12
+ def initialize(bin_path = "ejson")
13
+ @bin_path = bin_path
14
+ end
15
+
16
+ def call(file_path)
17
+ return unless File.exist?(file_path)
18
+
19
+ raw_content = nil
20
+
21
+ stdout, stderr, status = Open3.capture3("#{bin_path} decrypt #{file_path}")
22
+
23
+ if status.success?
24
+ raw_content = JSON.parse(stdout.chomp)
25
+ else
26
+ Kernel.warn "Failed to decrypt #{file_path}: #{stderr}"
27
+ end
28
+
29
+ return unless raw_content
30
+
31
+ raw_content.deep_transform_keys do |key|
32
+ if key[0] == "_"
33
+ key[1..]
34
+ else
35
+ key
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
data/lib/anyway/env.rb ADDED
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anyway
4
+ # Parses environment variables and provides
5
+ # method-like access
6
+ class Env
7
+ using RubyNext
8
+ using Anyway::Ext::DeepDup
9
+ using Anyway::Ext::Hash
10
+
11
+ class << self
12
+ def from_hash(hash, prefix: nil, memo: {})
13
+ hash.each do |key, value|
14
+ prefix_with_key = (prefix && !prefix.empty?) ? "#{prefix}_#{key.to_s.upcase}" : key.to_s.upcase
15
+
16
+ if value.is_a?(Hash)
17
+ from_hash(value, prefix: "#{prefix_with_key}_", memo:)
18
+ else
19
+ memo[prefix_with_key] = value.to_s
20
+ end
21
+ end
22
+
23
+ memo
24
+ end
25
+ end
26
+
27
+ include Tracing
28
+
29
+ attr_reader :data, :traces, :type_cast, :env_container
30
+
31
+ def initialize(type_cast: AutoCast, env_container: ENV)
32
+ @type_cast = type_cast
33
+ @data = {}
34
+ @traces = {}
35
+ @env_container = env_container
36
+ end
37
+
38
+ def clear
39
+ data.clear
40
+ traces.clear
41
+ end
42
+
43
+ def fetch(prefix)
44
+ return data[prefix].deep_dup if data.key?(prefix)
45
+
46
+ Tracing.capture do
47
+ data[prefix] = parse_env(prefix)
48
+ end.then do |trace|
49
+ traces[prefix] = trace
50
+ end
51
+
52
+ data[prefix].deep_dup
53
+ end
54
+
55
+ def fetch_with_trace(prefix)
56
+ [fetch(prefix), traces[prefix]]
57
+ end
58
+
59
+ private
60
+
61
+ def parse_env(prefix)
62
+ match_prefix = prefix.empty? ? prefix : "#{prefix}_"
63
+ env_container.each_pair.with_object({}) do |(key, val), data|
64
+ next unless key.start_with?(match_prefix)
65
+
66
+ path = key.sub(/^#{match_prefix}/, "").downcase
67
+
68
+ paths = path.split("__")
69
+ trace!(:env, *paths, key:) { data.bury(type_cast.call(val), *paths) }
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anyway
4
+ module Ext
5
+ # Extend Object through refinements
6
+ module DeepDup
7
+ refine ::Hash do
8
+ # Based on ActiveSupport http://api.rubyonrails.org/classes/Hash.html#method-i-deep_dup
9
+ def deep_dup
10
+ each_with_object(dup) do |(key, value), hash|
11
+ hash[key] = if value.is_a?(::Hash) || value.is_a?(::Array)
12
+ value.deep_dup
13
+ else
14
+ value
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ refine ::Array do
21
+ # From ActiveSupport http://api.rubyonrails.org/classes/Array.html#method-i-deep_dup
22
+ def deep_dup
23
+ map do |value|
24
+ if value.is_a?(::Hash) || value.is_a?(::Array)
25
+ value.deep_dup
26
+ else
27
+ value
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ refine ::Object do
34
+ def deep_dup
35
+ dup
36
+ end
37
+ end
38
+
39
+ refine ::Module do
40
+ def deep_dup
41
+ self
42
+ end
43
+ end
44
+
45
+ using self
46
+ end
47
+ end
48
+ end